@textlint/kernel
Version:
textlint kernel is core logic by pure JavaScript.
354 lines (334 loc) • 11.3 kB
text/typescript
import type {
TextlintRuleContextReportFunctionArgs,
TextlintRuleError,
TextlintRuleErrorPaddingLocation,
TextlintSourceCode,
TextlintRuleErrorDetails
} from "@textlint/types";
import { TxtNode } from "@textlint/ast-node-types";
import { invariant } from "../util/invariant";
import { throwIfTesting } from "@textlint/feature-flag";
import { isTextlintRuleErrorPaddingLocation } from "../context/TextlintRulePaddingLocator";
export interface ReportMessage {
ruleId: string;
node: any;
severity: number;
ruleError: TextlintRuleError;
}
// relative padding location info
export type SourceLocationPaddingIR =
| TextlintRuleErrorPaddingLocation
| {
// if no padding, line and column will be 0
line: number;
column: number;
};
const assertReportArgs = (reportArgs: Pick<TextlintRuleContextReportFunctionArgs, "node" | "ruleError" | "ruleId">) => {
const { ruleError, ruleId } = reportArgs;
const errorPrefix = `[${ruleId}]` || "";
const padding = ruleError;
/*
FIXME: It is old and un-document way
new RuleError("message", index);
*/
if (typeof padding === "number") {
throw new Error(`${errorPrefix} This is un-document way:
report(node, new RuleError("message", index);
Please use { index }:
report(node, new RuleError("message", {
index: paddingLineColumn
});
`);
}
// when running from textlint-tester, assert
if (padding.line === undefined && padding.column !== undefined) {
// FIXME: Backward compatible <= textlint.5.5
throwIfTesting(`${errorPrefix} Have to use a sets with "line" and "column".
See FAQ: https://github.com/textlint/textlint/blob/master/docs/faq/line-column-or-index.md
report(node, new RuleError("message", {
line: paddingLineNumber,
column: paddingLineColumn
});
OR use "index" property insteadof only "column".
report(node, new RuleError("message", {
index: paddingLineColumn
});
`);
}
// When either one of {column, line} or {index} is not used, throw error
if ((padding.line !== undefined || padding.column !== undefined) && padding.index !== undefined) {
// Introduced textlint 5.6
// https://github.com/textlint/textlint/releases/tag/5.6.0
// Always throw Error
throw new Error(`${errorPrefix} Have to use one of {line, column} or {index}.
You should use either one:
use "line" and "column" property
report(node, new RuleError("message", {
line: paddingLineNumber,
column: paddingLineColumn
});
OR
use "index" property
report(node, new RuleError("message", {
index: paddingIndexValue
});
`);
}
// NaN check
if (padding.line !== undefined && Number.isNaN(padding.line)) {
throw new Error("reported { line } is NaN");
}
if (padding.column !== undefined && Number.isNaN(padding.column)) {
throw new Error("reported { column } is NaN");
}
if (padding.index !== undefined && Number.isNaN(padding.index)) {
throw new Error("reported { index } is NaN");
}
// invalid padding check
if (padding.padding !== undefined) {
invariant(isTextlintRuleErrorPaddingLocation(padding.padding), "reported { padding } is invalid format.");
}
};
/**
* Create intermediate represent to resolve location
* `padding` property is pass threw
* Convert `index` to IR
* Convert `column` and `line` to IR
* @param errorDetails
*/
const createPaddingIR = (errorDetails: TextlintRuleErrorDetails): SourceLocationPaddingIR | null => {
if ("padding" in errorDetails && typeof errorDetails.padding === "object") {
return errorDetails.padding;
}
// when use {index}
if (errorDetails.index !== undefined) {
const paddingIndex = errorDetails.index;
return {
type: "TextlintRuleErrorPaddingLocation",
isAbsolute: false,
range: [paddingIndex, paddingIndex + 1]
};
}
// when use {line, column}
if (errorDetails.line !== undefined && errorDetails.column !== undefined) {
// when report with padding {line, column}, message.column should be 0 + padding.column.
// In other word, padding line > 0 and message.column start with 0.
return {
line: Math.max(errorDetails.line, 0),
column: Math.max(errorDetails.column, 0)
};
}
// when use { line } only
if (errorDetails.line !== undefined && errorDetails.line > 0) {
return {
line: Math.max(errorDetails.line, 0),
column: 0
};
}
/*
when use { column } only
FIXME: backward compatible @ un-document
Remove next version 6?
```
new RuleError({
column: index
});
```
*/
if (errorDetails.column !== undefined && errorDetails.column > 0) {
return {
line: 0,
column: Math.max(errorDetails.column, 0)
};
}
// No Padding
return null;
};
const toAbsoluteLocation = ({
source,
node,
paddingIR
}: {
source: TextlintSourceCode;
node: TxtNode;
paddingIR: SourceLocationPaddingIR | null;
}): PublicOutputLocationResult => {
if (!paddingIR) {
return {
range: node.range,
loc: {
start: {
line: node.loc.start.line,
column: node.loc.start.column + 1
},
end: {
line: node.loc.end.line,
column: node.loc.end.column + 1
}
}
};
}
const nodeRange = node.range;
const line = node.loc.start.line;
const column = node.loc.start.column;
// When { line, column } padding
if ("line" in paddingIR && "column" in paddingIR) {
const absoluteStartPosition = {
line: line + paddingIR.line,
column: column + paddingIR.column
};
const absoluteIndex = source.positionToIndex(absoluteStartPosition);
if (Number.isNaN(absoluteIndex)) {
throw new Error("absoluteIndex is NaN in { line, column }");
}
const absoluteRange = [absoluteIndex, absoluteIndex + 1] as const;
const absoluteLocation = source.rangeToLocation(absoluteRange);
if (Number.isNaN(absoluteLocation)) {
throw new Error("absoluteLocation is NaN in { line, column }");
}
return {
range: absoluteRange,
loc: {
start: {
line: absoluteLocation.start.line,
column: absoluteLocation.start.column + 1
},
end: {
line: absoluteLocation.end.line,
column: absoluteLocation.end.column + 1
}
}
};
}
// When { range } padding
if ("range" in paddingIR) {
const absoluteRange = [nodeRange[0] + paddingIR.range[0], nodeRange[0] + paddingIR.range[1]] as [
number,
number
];
const absoluteLocation = source.rangeToLocation(absoluteRange);
if (Number.isNaN(absoluteLocation)) {
throw new Error("absoluteLocation is NaN in { range }");
}
return {
range: absoluteRange,
loc: {
start: {
line: absoluteLocation.start.line,
column: absoluteLocation.start.column + 1
},
end: {
line: absoluteLocation.end.line,
column: absoluteLocation.end.column + 1
}
}
};
}
// When { loc } padding
if ("loc" in paddingIR) {
// relative from node start position
const absoluteStartPosition = {
line: line + paddingIR.loc.start.line,
column: column + paddingIR.loc.start.column
};
const absoluteEndPosition = {
line: line + paddingIR.loc.end.line,
column: column + paddingIR.loc.end.column
};
const absoluteLocation = source.locationToRange({
start: absoluteStartPosition,
end: absoluteEndPosition
});
const absoluteRange = absoluteLocation;
if (Number.isNaN(absoluteRange[0]) || Number.isNaN(absoluteRange[1])) {
throw new Error("absoluteLocation is NaN in { loc }");
}
return {
range: absoluteRange,
loc: {
start: {
line: absoluteStartPosition.line,
column: absoluteStartPosition.column + 1
},
end: {
line: absoluteEndPosition.line,
column: absoluteEndPosition.column + 1
}
}
};
}
return {
range: node.range,
loc: node.loc
};
};
/**
* Adjust `fix` command range
* if `fix.isAbsolute` is not absolute position, adjust the position from the `node`.
*/
export const resolveFixCommandLocation = ({ node, ruleError }: { node: TxtNode; ruleError: TextlintRuleError }) => {
const nodeRange = node.range;
// if not found `fix`, return empty object
if (ruleError.fix === undefined) {
return {}; // TODO: it should be undefined?
}
invariant(typeof ruleError.fix === "object", "fix should be FixCommand object");
// if absolute position return self
if (ruleError.fix.isAbsolute) {
return {
// remove other property that is not related `fix`
// the return object will be merged by `Object.assign`
fix: {
range: ruleError.fix.range,
text: ruleError.fix.text
}
};
}
// if relative position return adjusted position
return {
// fix(command) is relative from node's range
fix: {
range: [nodeRange[0] + ruleError.fix.range[0], nodeRange[0] + ruleError.fix.range[1]] as const,
text: ruleError.fix.text
}
};
};
/**
* Resolved Location's loc is 1-based line and column
* It aims to compat to output of textlint.
* Internally, textlint use 0-based column value, but output is 1-based column.
*/
export type PublicOutputLocationResult = {
// 0-based
range: readonly [startIndex: number, endIndex: number];
loc: {
start: {
line: number; // 1-based
column: number; // 1-based
};
end: {
line: number; // 1-based
column: number; // 1-based
};
};
};
/**
* resolve `padding`(includes deprecated `index`, `column`, `line`) to absolute location
* padding + node's start position
* also, line and column in loc will be 1-based index values
* @param args
*/
export const resolveLocation = (
args: {
source: TextlintSourceCode;
} & Pick<TextlintRuleContextReportFunctionArgs, "node" | "ruleError" | "ruleId">
): PublicOutputLocationResult => {
const { node, ruleError } = args;
assertReportArgs(args);
const paddingIR = createPaddingIR(ruleError);
return toAbsoluteLocation({
source: args.source,
node,
paddingIR
});
};