Skip to content

Commit

Permalink
Implement support embedded templates (#2483)
Browse files Browse the repository at this point in the history
  • Loading branch information
ventuno committed Aug 31, 2022
1 parent f0b948b commit 2ba29bd
Show file tree
Hide file tree
Showing 14 changed files with 4,030 additions and 333 deletions.
186 changes: 186 additions & 0 deletions lib/extract-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { parseTemplates } from 'ember-template-imports/lib/parse-templates.js';
import * as util from 'ember-template-imports/src/util.js';

export const SUPPORTED_EXTENSIONS = ['.js', '.ts', '.gjs', '.gts'];
const LOCATION_START = Object.freeze({ line: 0, column: 0 });
/**
* Processes results and corrects for template location offsets.
*
* @typedef {object} TemplateInfo
* @property {number} line
* @property {number} column
* @property {string} template
* @property {boolean} isEmbedded
*
* @param {string} moduleSource
* @param {string} relativePath
*
* @returns {TemplateInfo[]}
*/
export function extractTemplates(moduleSource, relativePath) {
// If no relativePath is present, assuming we might have templates.
let mightHaveTemplates = relativePath ? isSupportedScriptFileExtension(relativePath) : true;

if (!mightHaveTemplates) {
return [makeTemplateInfo(LOCATION_START, moduleSource)];
}

let parsed = parseTemplates(moduleSource, relativePath, {
templateTag: util.TEMPLATE_TAG_NAME,
templateLiteral: [
{
importPath: 'ember-cli-htmlbars',
importIdentifier: 'hbs',
},
{
importPath: '@ember/template-compilation',
importIdentifier: 'hbs',
},
{
importPath: util.TEMPLATE_LITERAL_MODULE_SPECIFIER,
importIdentifier: util.TEMPLATE_LITERAL_IDENTIFIER,
},
{
importPath: 'ember-cli-htmlbars-inline-precompile',
importIdentifier: 'default',
},
{
importPath: 'htmlbars-inline-precompile',
importIdentifier: 'default',
},
{
importPath: '@ember/template-compilation',
importIdentifier: 'precompileTemplate',
},
],
});

// If after parsing we found no templates and we had no relative path,
// then we assume we had a hbs file in input.
if (parsed.length === 0 && !relativePath) {
return [makeTemplateInfo(LOCATION_START, moduleSource)];
}

let result = parsed.map((templateInfo) => {
let { start, end } = templateInfo;
let templateStart = start.index + start[0].length;

let template = moduleSource.slice(templateStart, end.index);

return makeTemplateInfo(
coordinatesOf(moduleSource, templateStart, end.index),
template,
templateInfo,
true
);
});

return result;
}

/**
* @param {object} location
* @param {number} location.line
* @param {number} location.column
* @param {string} template
* @param {object} templateInfo
* @param {boolean} isEmbedded
*
* @returns {TemplateInfo}
*/
function makeTemplateInfo({ line, column }, template, templateInfo, isEmbedded) {
return {
line,
column,
template,
isEmbedded,
isStrictMode: !isEmbedded || isStrictMode(templateInfo),
};
}

/**
* @param {object} templateInfo
*
* @returns {boolean}
*/
export function isStrictMode(templateInfo) {
return (
(templateInfo.importIdentifier === util.TEMPLATE_LITERAL_IDENTIFIER &&
templateInfo.importPath === util.TEMPLATE_LITERAL_MODULE_SPECIFIER) ||
templateInfo.type === 'template-tag'
);
}

export function isSupportedScriptFileExtension(filePath = '') {
return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
}

export function coordinatesOf(text, offset) {
const contentBeforeTemplateStart = text.slice(0, offset).split('\n');
return {
line: contentBeforeTemplateStart.length,
column: contentBeforeTemplateStart[contentBeforeTemplateStart.length - 1].length,
};
}

/**
* @param {TemplateInfo} templateInfo
* @param {object} result
* @param {number} result.line
* @param {number} result.endLine
* @param {number} result.column
* @param {number} result.endColumn
*
* @returns {object}
*/
export function coordinatesOfResult(templateInfo, result) {
/**
* Given the sample source code:
* 1 import { hbs } from 'ember-cli-htmlbars'\n
* 2 export class SomeComponent extends Component<Args> {\n
* 3 <template>\n
* 4 {{debugger}}\n
* 5 </template>\n
* 6 }
*
* The extracted template will be:
* 1 \n
* 2 {{debugger}}\n
*
* The coordinates of the template in the source file are: { line: 3, column: 14 }.
* The coordinates of the error in the template are: { line: 2, column: 4 }.
*
* Thus, we need to always subtract one before adding the template location.
*/
const line = result.line + templateInfo.line - 1;
const endLine = result.endLine + templateInfo.line - 1;

/**
* Given the sample source code:
* 1 import { hbs } from 'ember-cli-htmlbars'\n
* 2 export class SomeComponent extends Component<Args> {\n
* 3 <template>{{debugger}}\n
* 4 </template>\n
* 5 }
*
* The extracted template will be:
* 1 {{debugger}}\n
*
* The coordinates of the template in the source file are: { line: 3, column: 14 }.
* The coordinates of the error in the template are: { line: 1, column: 0 }.
*
* Thus, if the error is found on the first line of a template,
* then we need to add the column location to the result column location.
*
* Any result > line 1 will not require any column correction.
*/
const column = result.line === 1 ? result.column + templateInfo.column : result.column;
const endColumn = result.line === 1 ? result.endColumn + templateInfo.column : result.endColumn;

return {
line,
endLine,
column,
endColumn,
};
}
10 changes: 8 additions & 2 deletions lib/helpers/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fs from 'node:fs';
import path from 'node:path';
import yargs from 'yargs';

import { SUPPORTED_EXTENSIONS } from '../extract-templates.js';
import camelize from './camelize.js';

const STDIN = '/dev/stdin';
Expand Down Expand Up @@ -104,7 +105,12 @@ export function parseArgv(_argv) {
'ignore-pattern': {
describe: 'Specify custom ignore pattern (can be disabled with --no-ignore-pattern)',
type: 'array',
default: ['**/dist/**', '**/tmp/**', '**/node_modules/**'],
default: [
'**/dist/**',
'**/tmp/**',
'**/node_modules/**',
...SUPPORTED_EXTENSIONS.map((ext) => `**/*${ext}`),
],
},
'no-inline-config': {
describe: 'Prevent inline configuration comments from changing config or rules',
Expand Down Expand Up @@ -228,7 +234,7 @@ export function getPossibleOptionNames(specifiedOptions) {
}

function executeGlobby(workingDir, pattern, ignore) {
let supportedExtensions = new Set(['.hbs', '.handlebars']);
let supportedExtensions = new Set(['.hbs', '.handlebars', ...SUPPORTED_EXTENSIONS]);

// `--no-ignore-pattern` results in `ignorePattern === [false]`
let options =
Expand Down
58 changes: 38 additions & 20 deletions lib/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parse, transform } from 'ember-template-recast';

import ModuleStatusCache from './-private/module-status-cache.js';
import TodoHandler from './-private/todo-handler.js';
import { extractTemplates, coordinatesOfResult } from './extract-templates.js';
import PrettyFormatter from './formatters/pretty.js';
import { getProjectConfig, getRuleFromString } from './get-config.js';
import EditorConfigResolver from './get-editor-config.js';
Expand Down Expand Up @@ -311,32 +312,49 @@ export default class Linter {
let hasBOM = options.source.codePointAt(0) === 0xfe_ff;
let source = hasBOM ? options.source.slice(1) : options.source;

let templateAST;
let templateInfos = extractTemplates(source, options.filePath);

try {
templateAST = parse(source);
} catch (error) {
let message = buildErrorMessage(options.filePath, error);
results.push(message);
return results;
}

let { failures, rules } = this.buildRules({
fileConfig,
filePath: options.filePath,
configResolver: options.configResolver,
rawSource: source,
});
results.push(...failures);
for (let templateInfo of templateInfos) {
let templateAST;

for (let rule of rules) {
try {
let visitor = await rule.getVisitor();
transform(templateAST, () => visitor);
results.push(...rule.results);
templateAST = parse(templateInfo.template);
} catch (error) {
let message = buildErrorMessage(options.filePath, error);
results.push(message);
return results;
}

let { failures, rules } = this.buildRules({
fileConfig,
filePath: options.filePath,
configResolver: options.configResolver,
rawSource: templateInfo.template,
isStrictMode: templateInfo.isStrictMode,
});
results.push(...failures);

for (let rule of rules) {
try {
let visitor = await rule.getVisitor();
transform(templateAST, () => visitor);

// apply offsets for embedded templates
if (templateInfo.isEmbedded) {
for (let result of rule.results) {
const resultCoordinates = coordinatesOfResult(templateInfo, result);
result.line = resultCoordinates.line;
result.endLine = resultCoordinates.endLine;
result.column = resultCoordinates.column;
result.endColumn = resultCoordinates.endColumn;
}
}

results.push(...rule.results);
} catch (error) {
let message = buildErrorMessage(options.filePath, error);
results.push(message);
}
}
}

Expand Down
8 changes: 5 additions & 3 deletions lib/rules/_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export default class {
this.results = [];
this.mode = options.shouldFix ? 'fix' : 'report';

this.isStrictMode = options.isStrictMode;

this.workingDir = options.workingDir;
// TODO: add a deprecation for accessing _filePath
this.filePath = this._filePath = options.filePath;
// To support DOM-scoped configuration instructions, we need to maintain
// a stack of our configurations so we can restore the previous one when
// the current one goes out of scope. The current one is duplicated in
Expand All @@ -58,9 +63,6 @@ export default class {
this.config = this.parseConfig(options.config);
this._configStack = [this.config];

this.workingDir = options.workingDir;
// TODO: add a deprecation for accessing _filePath
this.filePath = this._filePath = options.filePath;
this[MODULE_NAME] = options.moduleName;
this._rawSource = options.rawSource;

Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-implicit-this.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const ARGLESS_DEFAULT_BLUEPRINT = [

export default class NoImplicitThis extends Rule {
parseConfig(config) {
if (config === false || config === undefined) {
if (config === false || config === undefined || !this.isStrictMode) {
return false;
}

Expand Down

0 comments on commit 2ba29bd

Please sign in to comment.