eslint-plugin-ember-template-lint 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -24,14 +24,34 @@ Add `hbs` to the plugins section of your `.eslintrc` configuration file. You can
24
24
 
25
25
  ### 2. Modify your `.eslintrc.js`
26
26
 
27
+ you can use the presets from here https://github.com/ember-template-lint/ember-template-lint#presets
28
+ like this:
29
+ `plugin:ember-template-lint/<preset>`
30
+
31
+ to just use the `.template-lintrc.js` config file just use
32
+ `plugin:ember-template-lint/config`
33
+
34
+ to load ember-template-lint plugins in eslinrc you need to manually register them:
35
+ ```js
36
+ require('eslint-plugin-ember-template-lint/lib/ember-teplate-lint/config').registerPlugin('ember-template-lint-plugin-prettier');
37
+ ```
38
+
39
+ ## IMPORTANT
40
+ Do NOT set parser to anything in your .eslintrc.js as it will override the parser from here
41
+
42
+ for typescript parser you can extend its base config, then it will work correctly
43
+
27
44
  ```js
28
45
  // .eslintrc.js
29
46
  // optional:
30
47
  require('eslint-plugin-ember-template-lint/lib/ember-teplate-lint/config').registerPlugin('ember-template-lint-plugin-prettier');
31
48
 
49
+
32
50
  module.exports = {
33
51
  extends: [
34
52
  'eslint:recommended',
53
+ 'plugin:@typescript-eslint/base',
54
+ 'plugin:ember-template-lint/config',
35
55
  'plugin:ember-template-lint/recommended',
36
56
  //optional:
37
57
  'plugin:ember-template-lint/ember-template-lint-plugin-prettier:recommended'
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ root: true,
3
+
4
+ plugins: ['ember-template-lint'],
5
+
6
+ rules: {},
7
+
8
+ overrides: [
9
+ {
10
+ files: ['**/*.hbs'],
11
+ parser: require.resolve('../parser/hbs-parser'),
12
+ processor: 'ember-template-lint/noop'
13
+ },
14
+ {
15
+ files: ['**/*.gts', '**/*.gjs'],
16
+ parser: require.resolve('../parser/gts-parser'),
17
+ processor: 'ember-template-lint/noop'
18
+ },
19
+ ],
20
+ };
@@ -0,0 +1,20 @@
1
+ const info = require('../ember-teplate-lint/info');
2
+ const base = require('./base');
3
+
4
+ const configs = {};
5
+
6
+ Object.entries(info.configs).forEach(([name, config]) => {
7
+ Object.entries(config.rules).forEach(([name, conf]) => {
8
+ if (typeof conf == 'boolean') {
9
+ config.rules[name] = [conf ? 'error' : 'off'];
10
+ }
11
+ });
12
+ configs[name] = {...base, rules: config.rules};
13
+ });
14
+ configs['config'] = {
15
+ ...base,
16
+ rules: info.configuredRules
17
+ };
18
+
19
+ module.exports = configs;
20
+
@@ -0,0 +1,18 @@
1
+ const templateLintConfig = {
2
+ rules: {},
3
+ plugins: [],
4
+ };
5
+
6
+ function registerRule(name, config) {
7
+ templateLintConfig.rules[name] = config;
8
+ }
9
+
10
+ function registerPlugin(name) {
11
+ templateLintConfig.plugins.push(name);
12
+ }
13
+
14
+ module.exports = {
15
+ registerRule,
16
+ registerPlugin,
17
+ templateLintConfig
18
+ };
@@ -0,0 +1,111 @@
1
+ const synckit = require('synckit');
2
+ const syncFn = synckit.createSyncFn(require.resolve('./worker'));
3
+ const { runTemplateLint } = require('../rules/lint');
4
+ const { registerRule, templateLintConfig } = require('../ember-teplate-lint/config');
5
+
6
+ const lintWithEslintConfigs = syncFn({ config: templateLintConfig });
7
+ const lintConfigs = syncFn();
8
+
9
+ const activeRules = new Map();
10
+ const allMessages = {};
11
+
12
+ const reporter = {
13
+ setup(context) {
14
+ this.getSourceCode = context.getSourceCode;
15
+ },
16
+ report(message) {
17
+ message.meta = {
18
+ fixable: 'code'
19
+ };
20
+ allMessages[message.rule] = allMessages[message.rule] || [];
21
+ allMessages[message.rule].push(message);
22
+ }
23
+ };
24
+
25
+ class Rule {
26
+
27
+ constructor(name) {
28
+ this.create = this.create.bind(this);
29
+ this.name = name;
30
+ this.meta = {
31
+ fixable: 'code'
32
+ };
33
+ }
34
+ create(context) {
35
+ const rule = this;
36
+ let options = context.options;
37
+ if (options.length === 0) {
38
+ options = true;
39
+ }
40
+ registerRule(this.name, options);
41
+ const visitor = {
42
+ enter(node) {
43
+ if (!activeRules.get(node)) {
44
+ activeRules.set(node, 0);
45
+ const sourceCode = context.getSourceCode();
46
+ const { scopeManager } = sourceCode;
47
+ const scopes = scopeManager.scopes.filter(x => x.block.range[0] < node.range[0] && x.block.range[1] > node.range[1]);
48
+ const scopeVars = scopes.map(s => s.variables.map(x => x.name)).flat();
49
+ const filename = context.getPhysicalFilename();
50
+ const start = node.range[0] || 0;
51
+ reporter.setup(context);
52
+ runTemplateLint(node.contents, filename, reporter, scopeVars, start);
53
+ }
54
+ activeRules.set(node, activeRules.get(node) + 1);
55
+ },
56
+ exit(node) {
57
+ const messages = allMessages[rule.name] || [];
58
+ messages.forEach(m => context.report(m));
59
+ activeRules.set(node, activeRules.get(node) - 1);
60
+ allMessages[rule.name] = [];
61
+ },
62
+ };
63
+ return {
64
+ 'Program': (node) => node.isHbs && visitor.enter(node),
65
+ 'Program:exit': (node) => node.isHbs && visitor.exit(node),
66
+ '__TEMPLATE__Template': visitor.enter,
67
+ '__TEMPLATE__Template:exit': visitor.exit,
68
+ };
69
+ }
70
+ }
71
+
72
+ function createRules(rules) {
73
+ const created = rules.map(r => new Rule(r));
74
+ const map = {};
75
+ created.forEach(r => {
76
+ map[r.name] = r;
77
+ });
78
+ return map;
79
+ }
80
+
81
+ const configs = {
82
+ ...lintWithEslintConfigs.configs,
83
+ ...lintConfigs.configs
84
+ };
85
+
86
+ const rules = [...new Set([...lintConfigs.rules, ...lintWithEslintConfigs.rules])];
87
+
88
+ delete configs.recommended.overrides;
89
+
90
+ Object.values(configs).forEach((config) => {
91
+ const rules = {};
92
+ Object.entries(config.rules).forEach(([rule, conf]) => {
93
+ rules['ember-template-lint/' + rule] = conf;
94
+ });
95
+ config.rules = rules;
96
+ });
97
+
98
+
99
+ const configuredRules = {};
100
+ Object.entries(lintConfigs.configuredRules).forEach(([name, conf]) => {
101
+ configuredRules['ember-template-lint/' + name] = conf.config && [conf.severity];
102
+ if (typeof conf.config !== 'boolean') {
103
+ configuredRules['ember-template-lint/' + name].push(conf.config);
104
+ }
105
+ });
106
+
107
+ module.exports = {
108
+ configs: configs,
109
+ rules: createRules(rules),
110
+ configuredRules: configuredRules
111
+ };
@@ -0,0 +1,12 @@
1
+ const { runAsWorker } = require('synckit');
2
+
3
+ runAsWorker(async (options) => {
4
+ const Lint = await import('ember-template-lint');
5
+ const lint = new Lint.default(options);
6
+ await lint.loadConfig();
7
+ return {
8
+ configs: lint.config.loadedConfigurations,
9
+ rules: Object.keys(lint.config.loadedRules),
10
+ configuredRules: lint.config.rules,
11
+ };
12
+ });
@@ -0,0 +1,80 @@
1
+ const { preprocess, traverse, Source } = require('@glimmer/syntax');
2
+ const gts = require('ember-template-imports');
3
+ const typescriptParser = require('@typescript-eslint/parser');
4
+ const typescriptEstree = require('@typescript-eslint/typescript-estree');
5
+
6
+ function visitorKeysForAst(source, ast) {
7
+ const tokens = [];
8
+ const types = new Set();
9
+ traverse(ast, {
10
+ All(node) {
11
+ types.add(`__TEMPLATE__${node.type}`);
12
+ tokens.push(node);
13
+ }
14
+ });
15
+ ast.tokens = tokens;
16
+ const visitorKeys = {};
17
+ types.forEach((t) => {
18
+ // visitorKeys[t] = ['body', 'name', 'path', 'params', 'attributes', 'hash', 'modifiers', 'comments', 'value', 'program', 'inverse', 'children']
19
+ visitorKeys[t] = [];
20
+ });
21
+ tokens.forEach((node) => {
22
+ node.type = `__TEMPLATE__${node.type}`;
23
+ });
24
+ return visitorKeys;
25
+ }
26
+
27
+ function replaceRange(s, start, end, substitute) {
28
+ return s.substring(0, start) + substitute + s.substring(end);
29
+ }
30
+
31
+
32
+ module.exports = {
33
+ parseForESLint(code, options) {
34
+ let jsCode = code;
35
+ const templateInfos = gts.parseTemplates(jsCode, options.filePath);
36
+ templateInfos.forEach((tpl) => {
37
+ const range = [tpl.start.index, tpl.end.index + tpl.end[0].length];
38
+ tpl.range = range;
39
+ const template = jsCode.slice(...range);
40
+ const lines = template.split('\n');
41
+ lines.forEach((line, i) => {
42
+ lines[i] = ' '.repeat(line.length);
43
+ });
44
+ const emptyText = '[`' + lines.join('\n').slice(4) + '`]';
45
+ jsCode = replaceRange(jsCode, ...range, emptyText);
46
+ const source = new Source(tpl.contents);
47
+ const ast = preprocess(source, { mode: 'codemod' });
48
+ ast.range = [tpl.start.index + tpl.start[0].length + 1, tpl.end.index];
49
+ ast.contents = tpl.contents;
50
+ tpl.ast = ast;
51
+ });
52
+ const result = typescriptParser.parseForESLint(jsCode, options);
53
+ const visitorKeys = {...result.visitorKeys};
54
+ result.ast.tokens.forEach((token) => {
55
+ if (token.type === 'Template') {
56
+ const range = [token.range[0] - 1, token.range[1] + 1];
57
+ const template = templateInfos.find(t => t.range[0] >= range[0] && t.range[1] <= range[1]);
58
+ if (!template) return;
59
+ const ast = template.ast;
60
+ ast.loc = token.loc;
61
+ const source = new Source(code);
62
+ Object.assign(visitorKeys, visitorKeysForAst(source, ast));
63
+ Object.assign(token, ast);
64
+ }
65
+ });
66
+ typescriptEstree.simpleTraverse(result.ast, {
67
+ enter(node) {
68
+ if (node.type === 'TemplateLiteral') {
69
+ const range = [node.range[0] - 1, node.range[1] + 1];
70
+ const template = templateInfos.find(t => t.range[0] >= range[0] && t.range[1] <= range[1]);
71
+ if (!template) return;
72
+ const ast = template.ast;
73
+ Object.assign(node, ast);
74
+ }
75
+ }
76
+ });
77
+
78
+ return { ...result, visitorKeys };
79
+ }
80
+ };
@@ -0,0 +1,63 @@
1
+ const { preprocess, traverse, Source } = require('@glimmer/syntax');
2
+
3
+ class Scope {
4
+ type = 'global';
5
+ variables = [];
6
+ through= [];
7
+ set = new Map();
8
+ upper = null;
9
+ childScopes = [];
10
+ references = [];
11
+ block = null;
12
+ }
13
+
14
+ class ScopeManager {
15
+ globalScope = new Scope();
16
+ scopes = [this.globalScope];
17
+
18
+ acquire() {
19
+ return;
20
+ }
21
+
22
+ getDeclaredVariables() {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ module.exports = {
28
+ parseForESLint(code) {
29
+ const source = new Source(code);
30
+ const ast = preprocess(source, { mode: 'codemod' });
31
+ const tokens = [];
32
+ const comments = [];
33
+ const types = new Set();
34
+ types.add('Program');
35
+ traverse(ast, {
36
+ All(node) {
37
+ types.add(`__TEMPLATE__${node.type}`);
38
+ const span = source.spanFor(node.loc);
39
+ node.range = [span.getStart().offset, span.getEnd().offset];
40
+ if (node.type.toLowerCase().includes('comment')) {
41
+ comments.push(node);
42
+ return;
43
+ }
44
+ tokens.push(node);
45
+ }
46
+ });
47
+ ast.tokens = tokens;
48
+ ast.comments = comments;
49
+ const visitorKeys = {};
50
+ types.forEach((t) => {
51
+ visitorKeys[t] = [];
52
+ });
53
+ ast.type = 'Program';
54
+ ast.isHbs = true;
55
+ ast.contents = code;
56
+ tokens.slice(1).forEach((node) => {
57
+ node.type = `__TEMPLATE__${node.type}`;
58
+ });
59
+ const scope = new ScopeManager();
60
+ scope.globalScope.block = ast;
61
+ return { ast, scopeManager: scope, visitorKeys };
62
+ }
63
+ };
@@ -0,0 +1,11 @@
1
+
2
+
3
+ module.exports = {
4
+ preprocess: (text, filename) => {
5
+ return [{text: text, filename}];
6
+ },
7
+ postprocess: (messages) => {
8
+ return messages.flat();
9
+ },
10
+ supportsAutofix: true
11
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @typedef {{ line: number; character: number }} Position
3
+ */
4
+
5
+ // Helper class to convert line/column from and to offset
6
+ // taken and adapt from https://github.com/typed-ember/glint/blob/main/packages/core/src/language-server/util/position.ts
7
+ class DocumentLines {
8
+ /**
9
+ * @param {string} contents
10
+ */
11
+ constructor(contents) {
12
+ this.lineStarts = computeLineStarts(contents);
13
+ }
14
+
15
+ /**
16
+ * @param {Position} position
17
+ * @return {number}
18
+ */
19
+ positionToOffset(position) {
20
+ const { line, character } = position;
21
+ return this.lineStarts[line] + character;
22
+ }
23
+
24
+ /**
25
+ *
26
+ * @param {number} position
27
+ * @return {Position}
28
+ */
29
+ offsetToPosition(position) {
30
+ const lineStarts = this.lineStarts;
31
+ let line = 0;
32
+ while (line + 1 < lineStarts.length && lineStarts[line + 1] <= position) {
33
+ line++;
34
+ }
35
+ const character = position - lineStarts[line];
36
+ return { line, character };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @returns {number[]}
42
+ * @param {string} text
43
+ */
44
+ function computeLineStarts(text) {
45
+ const result = [];
46
+ let pos = 0;
47
+ let lineStart = 0;
48
+ while (pos < text.length) {
49
+ const ch = text.codePointAt(pos);
50
+ pos++;
51
+ switch (ch) {
52
+ case 13 /* carriageReturn */: {
53
+ if (text.codePointAt(pos) === 10 /* lineFeed */) {
54
+ pos++;
55
+ }
56
+ }
57
+ // falls through
58
+ case 10 /* lineFeed */: {
59
+ result.push(lineStart);
60
+ lineStart = pos;
61
+ break;
62
+ }
63
+ default: {
64
+ if (ch > 127 /* maxAsciiCharacter */ && isLineBreak(ch)) {
65
+ result.push(lineStart);
66
+ lineStart = pos;
67
+ }
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ result.push(lineStart);
73
+ return result;
74
+ }
75
+
76
+ /* istanbul ignore next */
77
+ /**
78
+ * @param {number} ch
79
+ * @return {boolean}
80
+ */
81
+ function isLineBreak(ch) {
82
+ // ES5 7.3:
83
+ // The ECMAScript line terminator characters are listed in Table 3.
84
+ // Table 3: Line Terminator Characters
85
+ // Code Unit Value Name Formal Name
86
+ // \u000A Line Feed <LF>
87
+ // \u000D Carriage Return <CR>
88
+ // \u2028 Line separator <LS>
89
+ // \u2029 Paragraph separator <PS>
90
+ // Only the characters in Table 3 are treated as line terminators. Other new line or line
91
+ // breaking characters are treated as white space but not as line terminators.
92
+ return (
93
+ ch === 10 /* lineFeed */ ||
94
+ ch === 13 /* carriageReturn */ ||
95
+ ch === 8232 /* lineSeparator */ ||
96
+ ch === 8233 /* paragraphSeparator */
97
+ );
98
+ }
99
+
100
+ module.exports = DocumentLines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ember-template-lint",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Provide linting for ember template",
5
5
  "keywords": [
6
6
  "eslint",
@@ -45,6 +45,6 @@
45
45
  },
46
46
  "license": "ISC",
47
47
  "files": [
48
- "lib/rules/*.js"
48
+ "lib/**/*.js"
49
49
  ]
50
50
  }