-
-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bonus: * enables type aware lints * prettier eslint plugin (with template tag prettier plugin) will just work for gts/gjs * can detect unused block params in templates * can detect undef vars in PathExpression * can add eslint directive comments in mustache or html disadvantage: * prettier will not work without template tag prettier plugin for gts/gjs files
- Loading branch information
Showing
12 changed files
with
1,772 additions
and
2,282 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
const gts = require('ember-template-tag'); | ||
const glimmer = require('@glimmer/syntax'); | ||
const DocumentLines = require('../utils/document'); | ||
const path = require('path'); | ||
// eslint-disable-next-line import/no-dynamic-require | ||
const glimmerVisitorKeys = require(path.join( | ||
path.dirname(require.resolve('@glimmer/syntax')), | ||
'lib/v1/visitor-keys' | ||
)).default; | ||
const babelParser = require('@babel/eslint-parser'); | ||
const typescriptParser = require('@typescript-eslint/parser'); | ||
// eslint-disable-next-line node/no-missing-require | ||
const TypescriptScope = require('@typescript-eslint/scope-manager'); | ||
const { Reference, Scope, Variable, Definition } = require('eslint-scope'); | ||
|
||
function findParentScope(scopeManager, nodePath) { | ||
let scope = null; | ||
let path = nodePath; | ||
while (path) { | ||
scope = scopeManager.acquire(path.node, true); | ||
if (scope) { | ||
return scope; | ||
} | ||
path = path.parentPath; | ||
} | ||
return null; | ||
} | ||
|
||
function findVarInParentScopes(scopeManager, nodePath, name) { | ||
let scope = null; | ||
let path = nodePath; | ||
while (path) { | ||
scope = scopeManager.acquire(path.node, true); | ||
if (scope && scope.set.has(name)) { | ||
break; | ||
} | ||
path = path.parentPath; | ||
} | ||
if (!scope) { | ||
return { scope: findParentScope(scopeManager, nodePath) }; | ||
} | ||
return { scope, variable: scope.set.get(name) }; | ||
} | ||
|
||
function registerNodeInScope(node, scope, variable) { | ||
const ref = new Reference(node, scope, Reference.READ); | ||
if (variable) { | ||
variable.references.push(ref); | ||
ref.resolved = variable; | ||
} else { | ||
scope.through.push(ref); | ||
scope.upper.through.push(ref); | ||
} | ||
scope.references.push(ref); | ||
} | ||
|
||
function traverse(visitorKeys, node, visitor) { | ||
const allVisitorKeys = visitorKeys; | ||
const queue = []; | ||
|
||
queue.push({ | ||
node, | ||
parent: null, | ||
parentKey: null, | ||
parentPath: null, | ||
}); | ||
|
||
while (queue.length > 0) { | ||
const currentPath = queue.pop(); | ||
|
||
visitor(currentPath); | ||
|
||
const visitorKeys = allVisitorKeys[currentPath.node.type]; | ||
if (!visitorKeys) { | ||
continue; | ||
} | ||
|
||
for (const visitorKey of visitorKeys) { | ||
const child = currentPath.node[visitorKey]; | ||
|
||
if (!child) { | ||
continue; | ||
} else if (Array.isArray(child)) { | ||
for (const item of child) { | ||
queue.push({ | ||
node: item, | ||
parent: currentPath.node, | ||
parentKey: visitorKey, | ||
parentPath: currentPath, | ||
}); | ||
} | ||
} else { | ||
queue.push({ | ||
node: child, | ||
parent: currentPath.node, | ||
parentKey: visitorKey, | ||
parentPath: currentPath, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function isUpperCase(char) { | ||
return char.toUpperCase() === char; | ||
} | ||
|
||
function preprocessGlimmerTemplates(info, code) { | ||
const templateInfos = info.replacements.map((r) => ({ | ||
range: r.original.contentRange, | ||
templateRange: r.original.range, | ||
replacedRange: r.replaced.range, | ||
})); | ||
const templateVisitorKeys = {}; | ||
const codeLines = new DocumentLines(code); | ||
const comments = []; | ||
for (const tpl of templateInfos) { | ||
const range = tpl.range; | ||
const template = code.slice(...range); | ||
const docLines = new DocumentLines(template); | ||
const ast = glimmer.preprocess(template, { mode: 'codemod' }); | ||
const allNodes = []; | ||
glimmer.traverse(ast, { | ||
All(node, path) { | ||
node.parent = path.parentNode; | ||
Check failure on line 125 in lib/parsers/gjs-parser.js GitHub Actions / build (ubuntu, 16.x)
|
||
allNodes.push(node); | ||
if (node.type === 'CommentStatement' || node.type === 'MustacheCommentStatement') { | ||
comments.push(node); | ||
} | ||
}, | ||
}); | ||
ast.content = template; | ||
const allNodeTypes = new Set(); | ||
for (const n of allNodes) { | ||
if (n.type === 'PathExpression') { | ||
n.head.range = [ | ||
range[0] + docLines.positionToOffset(n.head.loc.start), | ||
range[0] + docLines.positionToOffset(n.head.loc.end), | ||
]; | ||
n.head.loc = { | ||
start: codeLines.offsetToPosition(n.head.range[0]), | ||
end: codeLines.offsetToPosition(n.head.range[1]), | ||
}; | ||
} | ||
n.range = | ||
n.type === 'Template' | ||
? [tpl.replacedRange[0], tpl.replacedRange[1]] | ||
: [ | ||
range[0] + docLines.positionToOffset(n.loc.start), | ||
range[0] + docLines.positionToOffset(n.loc.end), | ||
]; | ||
|
||
n.start = n.range[0]; | ||
n.end = n.range[1]; | ||
n.loc = { | ||
start: codeLines.offsetToPosition(n.range[0]), | ||
end: codeLines.offsetToPosition(n.range[1]), | ||
}; | ||
if (n.type === 'Template') { | ||
n.loc.start = codeLines.offsetToPosition(tpl.templateRange[0]); | ||
n.loc.end = codeLines.offsetToPosition(tpl.templateRange[1]); | ||
} | ||
if ('blockParams' in n) { | ||
n.params = []; | ||
} | ||
if ('blockParams' in n && n.parent) { | ||
let part = code.slice(...n.parent.range); | ||
let start = n.parent.range[0]; | ||
let idx = part.indexOf('|') + 1; | ||
start += idx; | ||
part = part.slice(idx, -1); | ||
idx = part.indexOf('|'); | ||
part = part.slice(0, idx); | ||
for (const param of n.blockParams) { | ||
const regex = new RegExp(`\\b${param}\\b`); | ||
const match = part.match(regex); | ||
const range = [start + match.index, 0]; | ||
range[1] = range[0] + param.length; | ||
n.params.push({ | ||
type: 'BlockParam', | ||
name: param, | ||
range, | ||
parent: n, | ||
loc: { | ||
start: codeLines.offsetToPosition(range[0]), | ||
end: codeLines.offsetToPosition(range[1]), | ||
}, | ||
}); | ||
} | ||
} | ||
n.type = `Glimmer__${n.type}`; | ||
allNodeTypes.add(n.type); | ||
} | ||
ast.contents = template; | ||
tpl.ast = ast; | ||
} | ||
for (const [k, v] of Object.entries(glimmerVisitorKeys)) { | ||
templateVisitorKeys[`Glimmer__${k}`] = [...v]; | ||
} | ||
return { | ||
templateVisitorKeys, | ||
templateInfos, | ||
comments, | ||
}; | ||
} | ||
|
||
function convertAst(result, preprocessedResult, visitorKeys) { | ||
const templateInfos = preprocessedResult.templateInfos; | ||
let counter = 0; | ||
result.ast.comments.push(...preprocessedResult.comments); | ||
traverse(visitorKeys, result.ast, (path) => { | ||
const node = path.node; | ||
if ( | ||
node.type === 'ExpressionStatement' || | ||
node.type === 'StaticBlock' || | ||
node.type === 'TemplateLiteral' || | ||
node.type === 'ExportDefaultDeclaration' | ||
) { | ||
let range = node.range; | ||
if (node.type === 'ExportDefaultDeclaration') { | ||
range = [node.declaration.range[0], node.declaration.range[1]]; | ||
} | ||
|
||
const template = templateInfos.find( | ||
(t) => t.replacedRange[0] === range[0] && t.replacedRange[1] === range[1] | ||
); | ||
if (!template) { | ||
return null; | ||
} | ||
counter++; | ||
const ast = template.ast; | ||
Object.assign(node, ast); | ||
} | ||
|
||
if ('blockParams' in node) { | ||
const upperScope = findParentScope(result.scopeManager, path); | ||
const scope = result.isTypescript | ||
? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node) | ||
: new Scope(result.scopeManager, 'block', upperScope, node); | ||
for (const [i, b] of node.params.entries()) { | ||
const v = new Variable(b.name, scope); | ||
v.identifiers.push(b); | ||
v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param')); | ||
scope.variables.push(v); | ||
scope.set.set(b.name, v); | ||
} | ||
} | ||
|
||
if (node.type === 'Glimmer__PathExpression' && node.head.type === 'VarHead') { | ||
const name = node.head.name; | ||
if (glimmer.isKeyword(name)) { | ||
return null; | ||
} | ||
const { scope, variable } = findVarInParentScopes(result.scopeManager, path, name) || {}; | ||
if (scope) { | ||
node.head.parent = node; | ||
registerNodeInScope(node.head, scope, variable); | ||
} | ||
} | ||
if (node.type === 'Glimmer__ElementNode') { | ||
node.name = node.tag; | ||
const { scope, variable } = findVarInParentScopes(result.scopeManager, path, node.tag) || {}; | ||
if (scope && (variable || isUpperCase(node.tag[0]))) { | ||
registerNodeInScope(node, scope, variable); | ||
} | ||
} | ||
return null; | ||
}); | ||
|
||
if (counter !== templateInfos.length) { | ||
throw new Error('failed to process all templates'); | ||
} | ||
} | ||
|
||
module.exports = { | ||
parseForESLint(code, options, isTypescript) { | ||
let jsCode = code; | ||
const info = gts.transformForLint({ | ||
input: jsCode, | ||
templateTag: 'template', | ||
explicitMode: true, | ||
linterMode: true, | ||
}); | ||
jsCode = info.output; | ||
|
||
let result = null; | ||
result = isTypescript | ||
? typescriptParser.parseForESLint(jsCode, { ...options, ranges: true }) | ||
: babelParser.parseForESLint(jsCode, { ...options, ranges: true }); | ||
if (!info.replacements?.length) { | ||
return result; | ||
} | ||
const preprocessedResult = preprocessGlimmerTemplates(info, code); | ||
const { templateVisitorKeys } = preprocessedResult; | ||
const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys }; | ||
result.isTypescript = isTypescript; | ||
convertAst(result, preprocessedResult, visitorKeys); | ||
return { ...result, visitorKeys }; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
const gjsParser = require('./gjs-parser'); | ||
|
||
module.exports = { | ||
parseForESLint(code, options) { | ||
return gjsParser.parseForESLint(code, options, true); | ||
}, | ||
}; |
Oops, something went wrong.