-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathLineCounter.cs
279 lines (246 loc) · 9.64 KB
/
LineCounter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
namespace Semmle.Util
{
/// <summary>
/// An instance of this class is used to store the computed line count metrics (of
/// various different types) for a piece of text.
/// </summary>
public sealed class LineCounts
{
//#################### PROPERTIES ####################
#region
/// <summary>
/// The number of lines in the text that contain code.
/// </summary>
public int Code { get; set; }
/// <summary>
/// The number of lines in the text that contain comments.
/// </summary>
public int Comment { get; set; }
/// <summary>
/// The total number of lines in the text.
/// </summary>
public int Total { get; set; }
#endregion
//#################### PUBLIC METHODS ####################
#region
public override bool Equals(object? other)
{
return other is LineCounts rhs &&
Total == rhs.Total &&
Code == rhs.Code &&
Comment == rhs.Comment;
}
public override int GetHashCode()
{
return Total ^ Code ^ Comment;
}
public override string ToString()
{
return $"Total: {Total} Code: {Code} Comment: {Comment}";
}
#endregion
}
/// <summary>
/// This class can be used to compute line count metrics of various different types
/// (code, comment and total) for a piece of text.
/// </summary>
public static class LineCounter
{
//#################### NESTED CLASSES ####################
#region
/// <summary>
/// An instance of this class keeps track of the contextual information required during line counting.
/// </summary>
private class Context
{
/// <summary>
/// The index of the current character under consideration.
/// </summary>
public int CurIndex { get; set; }
/// <summary>
/// Whether or not the current line under consideration contains any code.
/// </summary>
public bool HasCode { get; set; }
/// <summary>
/// Whether or not the current line under consideration contains a comment.
/// </summary>
public bool HasComment { get; set; }
}
#endregion
//#################### PUBLIC METHODS ####################
#region
/// <summary>
/// Computes line count metrics for the specified input text.
/// </summary>
/// <param name="input">The input text for which to compute line count metrics.</param>
/// <returns>The computed metrics.</returns>
public static LineCounts ComputeLineCounts(string input)
{
var counts = new LineCounts();
var context = new Context();
char? cur, prev = null;
while ((cur = GetNext(input, context)) is not null)
{
if (IsNewLine(cur))
{
RegisterNewLine(counts, context);
cur = null;
}
else if (cur == '*' && prev == '/')
{
ReadMultiLineComment(input, counts, context);
cur = null;
}
else if (cur == '/' && prev == '/')
{
ReadEOLComment(input, context);
context.HasComment = true;
cur = null;
}
else if (cur == '"')
{
ReadRestOfString(input, context);
context.HasCode = true;
cur = null;
}
else if (cur == '\'')
{
ReadRestOfChar(input, context);
context.HasCode = true;
cur = null;
}
else if (!IsWhitespace(cur) && cur != '/') // exclude '/' to avoid counting comments as code
{
context.HasCode = true;
}
prev = cur;
}
// The final line of text should always be counted, even if it's empty.
RegisterNewLine(counts, context);
return counts;
}
#endregion
//#################### PRIVATE METHODS ####################
#region
/// <summary>
/// Gets the next character to be considered from the input text and updates the current character index accordingly.
/// </summary>
/// <param name="input">The input text for which we are computing line count metrics.</param>
/// <param name="context">The contextual information required during line counting.</param>
/// <returns></returns>
private static char? GetNext(string input, Context context)
{
return input is null || context.CurIndex >= input.Length ?
(char?)null :
input[context.CurIndex++];
}
/// <summary>
/// Determines whether or not the specified character equals '\n'.
/// </summary>
/// <param name="c">The character to test.</param>
/// <returns>true, if the specified character equals '\n', or false otherwise.</returns>
private static bool IsNewLine(char? c)
{
return c == '\n';
}
/// <summary>
/// Determines whether or not the specified character should be considered to be whitespace.
/// </summary>
/// <param name="c">The character to test.</param>
/// <returns>true, if the specified character should be considered to be whitespace, or false otherwise.</returns>
private static bool IsWhitespace(char? c)
{
return c == ' ' || c == '\t' || c == '\r';
}
/// <summary>
/// Consumes the input text up to the end of the current line (not including any '\n').
/// This is used to consume an end-of-line comment (i.e. a //-style comment).
/// </summary>
/// <param name="input">The input text.</param>
/// <param name="context">The contextual information required during line counting.</param>
private static void ReadEOLComment(string input, Context context)
{
char? c;
do
{
c = GetNext(input, context);
} while (c is not null && !IsNewLine(c));
// If we reached the end of a line (as opposed to reaching the end of the text),
// put the '\n' back so that it can be handled by the normal newline processing
// code.
if (IsNewLine(c))
--context.CurIndex;
}
/// <summary>
/// Consumes the input text up to the end of a multi-line comment.
/// </summary>
/// <param name="input">The input text.</param>
/// <param name="counts">The line count metrics for the input text.</param>
/// <param name="context">The contextual information required during line counting.</param>
private static void ReadMultiLineComment(string input, LineCounts counts, Context context)
{
char? cur = '\0', prev = null;
context.HasComment = true;
while (cur is not null && ((cur = GetNext(input, context)) != '/' || prev != '*'))
{
if (IsNewLine(cur))
{
RegisterNewLine(counts, context);
context.HasComment = true;
}
prev = cur;
}
}
/// <summary>
/// Consumes the input text up to the end of a character literal, e.g. '\t'.
/// </summary>
/// <param name="input">The input text.</param>
/// <param name="context">The contextual information required during line counting.</param>
private static void ReadRestOfChar(string input, Context context)
{
if (GetNext(input, context) == '\\')
{
GetNext(input, context);
}
GetNext(input, context);
}
/// <summary>
/// Consumes the input text up to the end of a string literal, e.g. "Wibble".
/// </summary>
/// <param name="input">The input text.</param>
/// <param name="context">The contextual information required during line counting.</param>
private static void ReadRestOfString(string input, Context context)
{
char? cur = '\0';
var numSlashes = 0;
while (cur is not null && ((cur = GetNext(input, context)) != '"' || (numSlashes % 2 != 0)))
{
if (cur == '\\')
++numSlashes;
else
numSlashes = 0;
}
}
/// <summary>
/// Updates the line count metrics when a newline character is seen, and resets
/// the code and comment flags in the context ready to process the next line.
/// </summary>
/// <param name="counts">The line count metrics for the input text.</param>
/// <param name="context">The contextual information required during line counting.</param>
private static void RegisterNewLine(LineCounts counts, Context context)
{
++counts.Total;
if (context.HasCode)
{
++counts.Code;
context.HasCode = false;
}
if (context.HasComment)
{
++counts.Comment;
context.HasComment = false;
}
}
#endregion
}
}