1package ast
2
3import (
4	"fmt"
5	"github.com/robertkrimen/otto/file"
6)
7
8// CommentPosition determines where the comment is in a given context
9type CommentPosition int
10
11const (
12	_        CommentPosition = iota
13	LEADING                  // Before the pertinent expression
14	TRAILING                 // After the pertinent expression
15	KEY                      // Before a key in an object
16	COLON                    // After a colon in a field declaration
17	FINAL                    // Final comments in a block, not belonging to a specific expression or the comment after a trailing , in an array or object literal
18	IF                       // After an if keyword
19	WHILE                    // After a while keyword
20	DO                       // After do keyword
21	FOR                      // After a for keyword
22	WITH                     // After a with keyword
23	TBD
24)
25
26// Comment contains the data of the comment
27type Comment struct {
28	Begin    file.Idx
29	Text     string
30	Position CommentPosition
31}
32
33// NewComment creates a new comment
34func NewComment(text string, idx file.Idx) *Comment {
35	comment := &Comment{
36		Begin:    idx,
37		Text:     text,
38		Position: TBD,
39	}
40
41	return comment
42}
43
44// String returns a stringified version of the position
45func (cp CommentPosition) String() string {
46	switch cp {
47	case LEADING:
48		return "Leading"
49	case TRAILING:
50		return "Trailing"
51	case KEY:
52		return "Key"
53	case COLON:
54		return "Colon"
55	case FINAL:
56		return "Final"
57	case IF:
58		return "If"
59	case WHILE:
60		return "While"
61	case DO:
62		return "Do"
63	case FOR:
64		return "For"
65	case WITH:
66		return "With"
67	default:
68		return "???"
69	}
70}
71
72// String returns a stringified version of the comment
73func (c Comment) String() string {
74	return fmt.Sprintf("Comment: %v", c.Text)
75}
76
77// Comments defines the current view of comments from the parser
78type Comments struct {
79	// CommentMap is a reference to the parser comment map
80	CommentMap CommentMap
81	// Comments lists the comments scanned, not linked to a node yet
82	Comments []*Comment
83	// future lists the comments after a line break during a sequence of comments
84	future []*Comment
85	// Current is node for which comments are linked to
86	Current Expression
87
88	// wasLineBreak determines if a line break occured while scanning for comments
89	wasLineBreak bool
90	// primary determines whether or not processing a primary expression
91	primary bool
92	// afterBlock determines whether or not being after a block statement
93	afterBlock bool
94}
95
96func NewComments() *Comments {
97	comments := &Comments{
98		CommentMap: CommentMap{},
99	}
100
101	return comments
102}
103
104func (c *Comments) String() string {
105	return fmt.Sprintf("NODE: %v, Comments: %v, Future: %v(LINEBREAK:%v)", c.Current, len(c.Comments), len(c.future), c.wasLineBreak)
106}
107
108// FetchAll returns all the currently scanned comments,
109// including those from the next line
110func (c *Comments) FetchAll() []*Comment {
111	defer func() {
112		c.Comments = nil
113		c.future = nil
114	}()
115
116	return append(c.Comments, c.future...)
117}
118
119// Fetch returns all the currently scanned comments
120func (c *Comments) Fetch() []*Comment {
121	defer func() {
122		c.Comments = nil
123	}()
124
125	return c.Comments
126}
127
128// ResetLineBreak marks the beginning of a new statement
129func (c *Comments) ResetLineBreak() {
130	c.wasLineBreak = false
131}
132
133// MarkPrimary will mark the context as processing a primary expression
134func (c *Comments) MarkPrimary() {
135	c.primary = true
136	c.wasLineBreak = false
137}
138
139// AfterBlock will mark the context as being after a block.
140func (c *Comments) AfterBlock() {
141	c.afterBlock = true
142}
143
144// AddComment adds a comment to the view.
145// Depending on the context, comments are added normally or as post line break.
146func (c *Comments) AddComment(comment *Comment) {
147	if c.primary {
148		if !c.wasLineBreak {
149			c.Comments = append(c.Comments, comment)
150		} else {
151			c.future = append(c.future, comment)
152		}
153	} else {
154		if !c.wasLineBreak || (c.Current == nil && !c.afterBlock) {
155			c.Comments = append(c.Comments, comment)
156		} else {
157			c.future = append(c.future, comment)
158		}
159	}
160}
161
162// MarkComments will mark the found comments as the given position.
163func (c *Comments) MarkComments(position CommentPosition) {
164	for _, comment := range c.Comments {
165		if comment.Position == TBD {
166			comment.Position = position
167		}
168	}
169	for _, c := range c.future {
170		if c.Position == TBD {
171			c.Position = position
172		}
173	}
174}
175
176// Unset the current node and apply the comments to the current expression.
177// Resets context variables.
178func (c *Comments) Unset() {
179	if c.Current != nil {
180		c.applyComments(c.Current, c.Current, TRAILING)
181		c.Current = nil
182	}
183	c.wasLineBreak = false
184	c.primary = false
185	c.afterBlock = false
186}
187
188// SetExpression sets the current expression.
189// It is applied the found comments, unless the previous expression has not been unset.
190// It is skipped if the node is already set or if it is a part of the previous node.
191func (c *Comments) SetExpression(node Expression) {
192	// Skipping same node
193	if c.Current == node {
194		return
195	}
196	if c.Current != nil && c.Current.Idx1() == node.Idx1() {
197		c.Current = node
198		return
199	}
200	previous := c.Current
201	c.Current = node
202
203	// Apply the found comments and futures to the node and the previous.
204	c.applyComments(node, previous, TRAILING)
205}
206
207// PostProcessNode applies all found comments to the given node
208func (c *Comments) PostProcessNode(node Node) {
209	c.applyComments(node, nil, TRAILING)
210}
211
212// applyComments applies both the comments and the future comments to the given node and the previous one,
213// based on the context.
214func (c *Comments) applyComments(node, previous Node, position CommentPosition) {
215	if previous != nil {
216		c.CommentMap.AddComments(previous, c.Comments, position)
217		c.Comments = nil
218	} else {
219		c.CommentMap.AddComments(node, c.Comments, position)
220		c.Comments = nil
221	}
222	// Only apply the future comments to the node if the previous is set.
223	// This is for detecting end of line comments and which node comments on the following lines belongs to
224	if previous != nil {
225		c.CommentMap.AddComments(node, c.future, position)
226		c.future = nil
227	}
228}
229
230// AtLineBreak will mark a line break
231func (c *Comments) AtLineBreak() {
232	c.wasLineBreak = true
233}
234
235// CommentMap is the data structure where all found comments are stored
236type CommentMap map[Node][]*Comment
237
238// AddComment adds a single comment to the map
239func (cm CommentMap) AddComment(node Node, comment *Comment) {
240	list := cm[node]
241	list = append(list, comment)
242
243	cm[node] = list
244}
245
246// AddComments adds a slice of comments, given a node and an updated position
247func (cm CommentMap) AddComments(node Node, comments []*Comment, position CommentPosition) {
248	for _, comment := range comments {
249		if comment.Position == TBD {
250			comment.Position = position
251		}
252		cm.AddComment(node, comment)
253	}
254}
255
256// Size returns the size of the map
257func (cm CommentMap) Size() int {
258	size := 0
259	for _, comments := range cm {
260		size += len(comments)
261	}
262
263	return size
264}
265
266// MoveComments moves comments with a given position from a node to another
267func (cm CommentMap) MoveComments(from, to Node, position CommentPosition) {
268	for i, c := range cm[from] {
269		if c.Position == position {
270			cm.AddComment(to, c)
271
272			// Remove the comment from the "from" slice
273			cm[from][i] = cm[from][len(cm[from])-1]
274			cm[from][len(cm[from])-1] = nil
275			cm[from] = cm[from][:len(cm[from])-1]
276		}
277	}
278}
279