Skip to content

Commit f43b491

Browse files
authored
feat: extract comments from ast (#209)
1 parent 3517848 commit f43b491

File tree

3 files changed

+225
-70
lines changed

3 files changed

+225
-70
lines changed

internal/linter/linter.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,15 @@ func RunLinter(programs []*compiler.Program, singleThreaded bool, allowFiles []s
5454
}
5555
}
5656
lintedFileCount.Add(1)
57+
58+
comments := make([]*ast.CommentRange, 0)
59+
utils.ForEachComment(&file.Node, func(comment *ast.CommentRange) { comments = append(comments, comment) }, file)
60+
5761
registeredListeners := make(map[ast.Kind][](func(node *ast.Node)), 20)
5862
{
5963
rules := getRulesForFile(file)
6064
// Create disable manager for this file
61-
disableManager := rule.NewDisableManager(file)
65+
disableManager := rule.NewDisableManager(file, comments)
6266

6367
for _, r := range rules {
6468
ctx := rule.RuleContext{

internal/rule/disable_manager.go

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package rule
22

33
import (
4-
"regexp"
54
"strings"
65

76
"github.com/microsoft/typescript-go/shim/ast"
@@ -33,72 +32,77 @@ type DisableManager struct {
3332
}
3433

3534
// NewDisableManager creates a new DisableManager for the given source file
36-
func NewDisableManager(sourceFile *ast.SourceFile) *DisableManager {
35+
func NewDisableManager(sourceFile *ast.SourceFile, comments []*ast.CommentRange) *DisableManager {
3736
dm := &DisableManager{
3837
sourceFile: sourceFile,
3938
disabledRules: make(map[string]bool),
4039
lineDisabledRules: make(map[int][]string),
4140
nextLineDisabledRules: make(map[int][]string),
4241
}
4342

44-
dm.parseESLintDirectives()
43+
dm.parseESLintDirectives(comments)
4544
return dm
4645
}
4746

4847
// parseESLintDirectives parses ESLint-style disable/enable comments from the source text
49-
func (dm *DisableManager) parseESLintDirectives() {
50-
if dm.sourceFile.Text() == "" {
48+
func (dm *DisableManager) parseESLintDirectives(comments []*ast.CommentRange) {
49+
if dm.sourceFile.Text() == "" || len(comments) == 0 {
5150
return
5251
}
5352

5453
text := dm.sourceFile.Text()
55-
lines := strings.Split(text, "\n")
5654

57-
// Regular expressions to match ESLint directives
58-
eslintDisableLineRe := regexp.MustCompile(`//\s*eslint-disable-line(?:\s+([^\r\n]+))?`)
59-
eslintDisableNextLineRe := regexp.MustCompile(`//\s*eslint-disable-next-line(?:\s+([^\r\n]+))?`)
60-
eslintDisableRe := regexp.MustCompile(`/\*\s*eslint-disable(?:\s+([^*]+))?\s*\*/`)
61-
eslintEnableRe := regexp.MustCompile(`/\*\s*eslint-enable(?:\s+([^*]+))?\s*\*/`)
62-
63-
for i, line := range lines {
64-
lineNum := i // 0-based line numbers
65-
66-
// Check for eslint-disable-line
67-
if matches := eslintDisableLineRe.FindStringSubmatch(line); matches != nil {
68-
rules := parseRuleNames(matches[1])
69-
if len(rules) == 0 {
70-
dm.lineDisabledRules[lineNum] = append(dm.lineDisabledRules[lineNum], "*")
71-
} else {
72-
dm.lineDisabledRules[lineNum] = append(dm.lineDisabledRules[lineNum], rules...)
73-
}
55+
for _, comment := range comments {
56+
var commentContent string
57+
switch comment.Kind {
58+
case ast.KindSingleLineCommentTrivia:
59+
commentContent = strings.TrimSpace(text[comment.Pos()+2 : comment.End()])
60+
case ast.KindMultiLineCommentTrivia:
61+
commentContent = strings.TrimSpace(text[comment.Pos()+2 : comment.End()-2])
7462
}
7563

76-
// Check for eslint-disable-next-line
77-
if matches := eslintDisableNextLineRe.FindStringSubmatch(line); matches != nil {
78-
rules := parseRuleNames(matches[1])
79-
nextLineNum := lineNum + 1
80-
if len(rules) == 0 {
81-
dm.nextLineDisabledRules[nextLineNum] = append(dm.nextLineDisabledRules[nextLineNum], "*")
82-
} else {
83-
dm.nextLineDisabledRules[nextLineNum] = append(dm.nextLineDisabledRules[nextLineNum], rules...)
84-
}
85-
}
86-
87-
// Check for eslint-disable (block comments)
88-
if matches := eslintDisableRe.FindStringSubmatch(line); matches != nil {
89-
rules := parseRuleNames(matches[1])
90-
if len(rules) == 0 {
91-
dm.disabledRules["*"] = true
64+
lineNum, _ := scanner.GetLineAndCharacterOfPosition(dm.sourceFile, comment.Pos())
65+
rulePos := 0
66+
67+
if strings.HasPrefix(commentContent, "eslint-disable") {
68+
rulePos += 14
69+
text := commentContent[rulePos:]
70+
71+
if strings.HasPrefix(text, "-line") {
72+
// Check for eslint-disable-line
73+
rulePos += 5
74+
rules := parseRuleNames(commentContent[rulePos:])
75+
if len(rules) == 0 {
76+
dm.lineDisabledRules[lineNum] = append(dm.lineDisabledRules[lineNum], "*")
77+
} else {
78+
dm.lineDisabledRules[lineNum] = append(dm.lineDisabledRules[lineNum], rules...)
79+
}
80+
} else if strings.HasPrefix(text, "-next-line") {
81+
// Check for eslint-disable-next-line
82+
rulePos += 10
83+
rules := parseRuleNames(commentContent[rulePos:])
84+
nextLineNum := lineNum + 1
85+
if len(rules) == 0 {
86+
dm.nextLineDisabledRules[nextLineNum] = append(dm.nextLineDisabledRules[nextLineNum], "*")
87+
} else {
88+
dm.nextLineDisabledRules[nextLineNum] = append(dm.nextLineDisabledRules[nextLineNum], rules...)
89+
}
9290
} else {
93-
for _, rule := range rules {
94-
dm.disabledRules[rule] = true
91+
// Check for eslint-disable (block comments)
92+
rules := parseRuleNames(commentContent[rulePos:])
93+
if len(rules) == 0 {
94+
dm.disabledRules["*"] = true
95+
} else {
96+
for _, rule := range rules {
97+
dm.disabledRules[rule] = true
98+
}
9599
}
96100
}
97-
}
101+
} else if strings.HasPrefix(commentContent, "eslint-enable") {
102+
rulePos += 13
98103

99-
// Check for eslint-enable (block comments)
100-
if matches := eslintEnableRe.FindStringSubmatch(line); matches != nil {
101-
rules := parseRuleNames(matches[1])
104+
// Check for eslint-enable (block comments)
105+
rules := parseRuleNames(commentContent[rulePos:])
102106
if len(rules) == 0 {
103107
// Enable all rules
104108
for key := range dm.disabledRules {

internal/utils/ts_api_utils.go

Lines changed: 171 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/microsoft/typescript-go/shim/ast"
55
"github.com/microsoft/typescript-go/shim/checker"
66
"github.com/microsoft/typescript-go/shim/core"
7+
"github.com/microsoft/typescript-go/shim/scanner"
78
)
89

910
func UnionTypeParts(t *checker.Type) []*checker.Type {
@@ -157,30 +158,54 @@ func GetWellKnownSymbolPropertyOfType(t *checker.Type, name string, typeChecker
157158
return checker.Checker_getPropertyOfType(typeChecker, t, checker.Checker_getPropertyNameForKnownSymbolName(typeChecker, name))
158159
}
159160

160-
/**
161-
* Checks if a given compiler option is enabled, accounting for whether all flags
162-
* (except `strictPropertyInitialization`) have been enabled by `strict: true`.
163-
* @category Compiler Options
164-
* @example
165-
* ```ts
166-
* const optionsLenient = {
167-
* noImplicitAny: true,
168-
* };
169-
*
170-
* isStrictCompilerOptionEnabled(optionsLenient, "noImplicitAny"); // true
171-
* isStrictCompilerOptionEnabled(optionsLenient, "noImplicitThis"); // false
172-
* ```
173-
* @example
174-
* ```ts
175-
* const optionsStrict = {
176-
* noImplicitThis: false,
177-
* strict: true,
178-
* };
179-
*
180-
* isStrictCompilerOptionEnabled(optionsStrict, "noImplicitAny"); // true
181-
* isStrictCompilerOptionEnabled(optionsStrict, "noImplicitThis"); // false
182-
* ```
183-
*/
161+
func GetChildren(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
162+
children := make([]*ast.Node, 0)
163+
164+
pos := node.Pos()
165+
node.ForEachChild(func(child *ast.Node) bool {
166+
childPos := child.Pos()
167+
if pos < childPos {
168+
scanner := scanner.GetScannerForSourceFile(sourceFile, pos)
169+
for pos < childPos {
170+
token := scanner.Token()
171+
tokenFullStart := scanner.TokenFullStart()
172+
tokenEnd := scanner.TokenEnd()
173+
children = append(children, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, node))
174+
pos = tokenEnd
175+
scanner.Scan()
176+
}
177+
}
178+
179+
children = append(children, child)
180+
pos = child.End()
181+
return false
182+
})
183+
return children
184+
}
185+
186+
// Checks if a given compiler option is enabled, accounting for whether all flags
187+
// (except `strictPropertyInitialization`) have been enabled by `strict: true`.
188+
//
189+
// @category Compiler Options
190+
//
191+
// @example
192+
//
193+
// const optionsLenient = {
194+
// noImplicitAny: true,
195+
// };
196+
//
197+
// isStrictCompilerOptionEnabled(optionsLenient, "noImplicitAny"); // true
198+
// isStrictCompilerOptionEnabled(optionsLenient, "noImplicitThis"); // false
199+
//
200+
// @example
201+
//
202+
// const optionsStrict = {
203+
// noImplicitThis: false,
204+
// strict: true,
205+
// };
206+
//
207+
// isStrictCompilerOptionEnabled(optionsStrict, "noImplicitAny"); // true
208+
// isStrictCompilerOptionEnabled(optionsStrict, "noImplicitThis"); // false
184209
func IsStrictCompilerOptionEnabled(
185210
options *core.CompilerOptions,
186211
option core.Tristate,
@@ -195,3 +220,125 @@ func IsStrictCompilerOptionEnabled(
195220
// isStrictCompilerOptionEnabled(options, "strictNullChecks"))
196221
// );
197222
}
223+
224+
// Port https://github.com/JoshuaKGoldberg/ts-api-utils/blob/491c0374725a5dd64632405efea101f20ed5451f/src/tokens.ts#L34
225+
//
226+
// Iterates over all tokens of `node`
227+
//
228+
// @category Nodes - Other Utilities
229+
//
230+
// @example
231+
//
232+
// declare const node: ts.Node;
233+
//
234+
// forEachToken(node, (token) => {
235+
// console.log("Found token:", token.getText());
236+
// });
237+
//
238+
// @param node The node whose tokens should be visited
239+
// @param callback Is called for every token contained in `node`
240+
func ForEachToken(node *ast.Node, callback func(token *ast.Node), sourceFile *ast.SourceFile) {
241+
queue := make([]*ast.Node, 0)
242+
243+
for {
244+
if ast.IsTokenKind(node.Kind) {
245+
callback(node)
246+
} else {
247+
children := GetChildren(node, sourceFile)
248+
for i := len(children) - 1; i >= 0; i-- {
249+
queue = append(queue, children[i])
250+
}
251+
}
252+
253+
if len(queue) == 0 {
254+
break
255+
}
256+
257+
node = queue[len(queue)-1]
258+
queue = queue[:len(queue)-1]
259+
}
260+
}
261+
262+
// Port https://github.com/JoshuaKGoldberg/ts-api-utils/blob/491c0374725a5dd64632405efea101f20ed5451f/src/comments.ts#L37C17-L37C31
263+
//
264+
// Iterates over all comments owned by `node` or its children.
265+
//
266+
// @category Nodes - Other Utilities
267+
//
268+
// @example
269+
//
270+
// declare const node: ts.Node;
271+
//
272+
// forEachComment(node, (fullText, comment) => {
273+
// console.log(`Found comment at position ${comment.pos}: '${fullText}'.`);
274+
// });
275+
func ForEachComment(node *ast.Node, callback func(comment *ast.CommentRange), sourceFile *ast.SourceFile) {
276+
fullText := sourceFile.Text()
277+
notJsx := sourceFile.LanguageVariant != core.LanguageVariantJSX
278+
279+
ForEachToken(
280+
node,
281+
func(token *ast.Node) {
282+
if token.Pos() == token.End() {
283+
return
284+
}
285+
286+
if token.Kind != ast.KindJsxText {
287+
pos := token.Pos()
288+
if pos == 0 {
289+
pos = len(scanner.GetShebang(fullText))
290+
}
291+
292+
for comment := range scanner.GetLeadingCommentRanges(&ast.NodeFactory{}, fullText, pos) {
293+
callback(&comment)
294+
}
295+
}
296+
297+
if notJsx || canHaveTrailingTrivia(token) {
298+
for comment := range scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, fullText, token.End()) {
299+
callback(&comment)
300+
}
301+
return
302+
}
303+
},
304+
sourceFile,
305+
)
306+
}
307+
308+
// Port https://github.com/JoshuaKGoldberg/ts-api-utils/blob/491c0374725a5dd64632405efea101f20ed5451f/src/comments.ts#L84
309+
//
310+
// Exclude trailing positions that would lead to scanning for trivia inside `JsxText`.
311+
// @internal
312+
func canHaveTrailingTrivia(token *ast.Node) bool {
313+
switch token.Kind {
314+
case ast.KindCloseBraceToken:
315+
// after a JsxExpression inside a JsxElement's body can only be other JsxChild, but no trivia
316+
return token.Parent.Kind != ast.KindJsxExpression || !isJsxElementOrFragment(token.Parent.Parent)
317+
case ast.KindGreaterThanToken:
318+
switch token.Parent.Kind {
319+
case ast.KindJsxClosingElement:
320+
case ast.KindJsxClosingFragment:
321+
// there's only trailing trivia if it's the end of the top element
322+
return !isJsxElementOrFragment(token.Parent.Parent.Parent)
323+
case ast.KindJsxOpeningElement:
324+
// if end is not equal, this is part of the type arguments list. in all other cases it would be inside the element body
325+
return token.End() != token.Parent.End()
326+
case ast.KindJsxOpeningFragment:
327+
return false // would be inside the fragment
328+
case ast.KindJsxSelfClosingElement:
329+
// if end is not equal, this is part of the type arguments list
330+
// there's only trailing trivia if it's the end of the top element
331+
return token.End() != token.Parent.End() || !isJsxElementOrFragment(token.Parent.Parent)
332+
}
333+
}
334+
335+
return true
336+
}
337+
338+
// Port https://github.com/JoshuaKGoldberg/ts-api-utils/blob/491c0374725a5dd64632405efea101f20ed5451f/src/comments.ts#L118
339+
//
340+
// Test if a node is a `JsxElement` or `JsxFragment`.
341+
// @internal
342+
func isJsxElementOrFragment(node *ast.Node) bool {
343+
return node.Kind == ast.KindJsxElement || node.Kind == ast.KindJsxFragment
344+
}

0 commit comments

Comments
 (0)