Skip to content

Commit ee66921

Browse files
authored
feat(options): support CodeBlock's comment (#69)
## Features - Add `checkCodeComment` options Example ```json { "rules": { "prh": { "checkCodeComment": ["js", "javascript"], "rulePaths" :["./prh.yml"] } } } ``` Support `CodeBlock`'s comment `//` and `/*`. ```js // $ is jquery ``` Limitation: It just support JavaScript Code, because I don't found any lang comment parser correctly. fix #38
1 parent d602eb5 commit ee66921

File tree

5 files changed

+285
-4
lines changed

5 files changed

+285
-4
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,35 @@ You can use `~` as Home directory abbreviation.
5252
- `checkBlockQuote`(optional) : Check `BlockQuote` node type (default: `false`)
5353
- `checkEmphasis`(optional) : Check `Emphasis` node type (default: `false`)
5454
- `checkHeader`(optional) : Check `Header` node type (default: `true`)
55+
- `checkCodeComment`(optional) : Check `CodeBlock` node's comment for `lang` (default: `[]`)
56+
- Set `lang` array like `["<codeblock-lang>"]` for checking CodeBlock
57+
- Example: If you want to check JavaScript CodeBlock, set `{ "checkCodeComment": ["js", "javascript"] }` Options
58+
- **Note:** Currently only support JavaScript CodeBlock
59+
- It use [@babel/parser](https://babeljs.io/docs/en/babel-parser)
60+
61+
**Examples**:
5562

5663
```json
5764
{
5865
"rules": {
5966
"prh": {
6067
"checkEmphasis": true,
61-
"checkHeader": false
68+
"checkHeader": false,
69+
"rulePaths" :["./prh.yml"]
70+
}
71+
}
72+
}
73+
```
74+
75+
Check CodeBlock's comment in JavaScript Code.
76+
77+
78+
```json
79+
{
80+
"rules": {
81+
"prh": {
82+
"checkCodeComment": ["js", "javascript"],
83+
"rulePaths" :["./prh.yml"]
6284
}
6385
}
6486
}
@@ -157,7 +179,7 @@ rules:
157179
158180
#### imports
159181
160-
prh.yml can import other yaml file
182+
`prh.yml` can import other yaml file
161183

162184
```yaml
163185
version: 1

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"prh"
3434
],
3535
"dependencies": {
36+
"@babel/parser": "^7.7.5",
3637
"prh": "^5.4.4",
3738
"textlint-rule-helper": "^2.1.1",
3839
"untildify": "^3.0.3"

src/textlint-rule-prh.js

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// LICENSE : MIT
22
"use strict";
33
import { RuleHelper } from "textlint-rule-helper";
4+
5+
import { parse } from "@babel/parser";
46
/**
57
* RegExp#flags polyfill
68
*/
@@ -21,7 +23,16 @@ const defaultOptions = {
2123
checkLink: false,
2224
checkBlockQuote: false,
2325
checkEmphasis: false,
24-
checkHeader: true
26+
checkHeader: true,
27+
/**
28+
* Check CodeBlock text
29+
* Default: []
30+
*/
31+
checkCodeComment: [],
32+
/**
33+
* Report parsing error for debug
34+
*/
35+
debug: false
2536
};
2637

2738
function createPrhEngine(rulePaths, baseDir) {
@@ -144,6 +155,38 @@ const getConfigBaseDir = context => {
144155
return textlintRcFilePath ? path.dirname(textlintRcFilePath) : process.cwd();
145156
};
146157

158+
/**
159+
* [Markdown] get actual code value from CodeBlock node
160+
* @param {Object} node
161+
* @param {string} raw raw value include CodeBlock syntax
162+
* @returns {string}
163+
*/
164+
function getUntrimmedCode(node, raw) {
165+
if (node.type !== "CodeBlock") {
166+
return node.value;
167+
}
168+
// Space indented CodeBlock that has not lang
169+
if (!node.lang) {
170+
return node.value;
171+
}
172+
173+
// If it is not markdown codeBlock, just use node.value
174+
if (!(raw.startsWith("```") && raw.endsWith("```"))) {
175+
if (node.value.endsWith("\n")) {
176+
return node.value;
177+
}
178+
return node.value + "\n";
179+
}
180+
// Markdown(remark) specific hack
181+
// https://github.com/wooorm/remark/issues/207#issuecomment-244620590
182+
const lines = raw.split("\n");
183+
// code lines without the first line and the last line
184+
const codeLines = lines.slice(1, lines.length - 1);
185+
// add last new line
186+
// \n```
187+
return codeLines.join("\n") + "\n";
188+
}
189+
147190
function reporter(context, userOptions = {}) {
148191
assertOptions(userOptions);
149192
const options = Object.assign({}, defaultOptions, userOptions);
@@ -159,6 +202,8 @@ function reporter(context, userOptions = {}) {
159202
const helper = new RuleHelper(context);
160203
const { Syntax, getSource, report, fixer, RuleError } = context;
161204
const ignoreNodeTypes = createIgnoreNodeTypes(options, Syntax);
205+
const codeCommentTypes = options.checkCodeComment ? options.checkCodeComment : defaultOptions.checkCodeComment;
206+
const isDebug = options.debug ? options.debug : defaultOptions.debug;
162207
return {
163208
[Syntax.Str](node) {
164209
if (helper.isChildNode(node, ignoreNodeTypes)) {
@@ -185,6 +230,71 @@ function reporter(context, userOptions = {}) {
185230
})
186231
);
187232
});
233+
},
234+
[Syntax.CodeBlock](node) {
235+
const lang = node.lang;
236+
if (!lang) {
237+
return;
238+
}
239+
const checkLang = codeCommentTypes.some(type => {
240+
return type === node.lang;
241+
});
242+
if (!checkLang) {
243+
return;
244+
}
245+
const rawText = getSource(node);
246+
const codeText = getUntrimmedCode(node, rawText);
247+
const sourceBlockDiffIndex = rawText !== node.value ? rawText.indexOf(codeText) : 0;
248+
const reportComment = comment => {
249+
// to get position from index
250+
// https://github.com/prh/prh/issues/29
251+
const dummyFilePath = "";
252+
// TODO: trim option for value?
253+
const text = comment.value;
254+
const makeChangeSet = prhEngine.makeChangeSet(dummyFilePath, text);
255+
forEachChange(makeChangeSet, text, ({ matchStartIndex, matchEndIndex, actual, expected, prh }) => {
256+
// If result is not changed, should not report
257+
if (actual === expected) {
258+
return;
259+
}
260+
261+
const suffix = prh !== null ? "\n" + prh : "";
262+
const messages = actual + " => " + expected + suffix;
263+
const commentIdentifier = comment.type === "CommentBlock" ? "/*" : "//";
264+
const commentStart = sourceBlockDiffIndex + comment.start + commentIdentifier.length;
265+
report(
266+
node,
267+
new RuleError(messages, {
268+
index: commentStart + matchStartIndex,
269+
fix: fixer.replaceTextRange(
270+
[commentStart + matchStartIndex, commentStart + matchEndIndex],
271+
expected
272+
)
273+
})
274+
);
275+
});
276+
};
277+
try {
278+
const AST = parse(codeText, {
279+
ranges: true,
280+
allowReturnOutsideFunction: true,
281+
allowAwaitOutsideFunction: true,
282+
allowUndeclaredExports: true,
283+
allowSuperOutsideMethod: true
284+
});
285+
const comments = AST.comments;
286+
if (!comments) {
287+
return;
288+
}
289+
comments.forEach(comment => {
290+
reportComment(comment);
291+
});
292+
} catch (error) {
293+
if (isDebug) {
294+
console.error(error);
295+
report(node, new RuleError(error.message));
296+
}
297+
}
188298
}
189299
};
190300
}

test/prh-rule-tester-test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ var tester = new TextLintTester();
55
// rule
66
import rule from "../src/textlint-rule-prh";
77
// ruleName, rule, { valid, invalid }
8+
const CODE_START_JS = "```js";
9+
const CODE_END = "```";
810
tester.run("prh", rule, {
911
valid: [
1012
{
@@ -25,6 +27,39 @@ tester.run("prh", rule, {
2527
rulePaths: [__dirname + "/fixtures/rule.yaml"],
2628
checkHeader: false
2729
}
30+
},
31+
{
32+
text: `${CODE_START_JS}\n" + "\n" + "\n" + "// JavaScript\n" + "var a = 1;\n${CODE_END}`,
33+
options: {
34+
checkCodeComment: ["js"],
35+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
36+
}
37+
},
38+
{
39+
text: `${CODE_START_JS}// jquery is wrong, but this check is not by default\n${CODE_END}`,
40+
options: {
41+
checkCodeComment: [],
42+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
43+
}
44+
},
45+
// empty code block
46+
{
47+
text: `${CODE_START_JS}${CODE_END}`,
48+
options: {
49+
checkCodeComment: ["js"],
50+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
51+
}
52+
},
53+
// The CodeBlock includes invalid syntax, it is just ignored
54+
{
55+
text: `${CODE_START_JS}
56+
+++++++
57+
// jquery
58+
${CODE_START_JS}`,
59+
options: {
60+
checkCodeComment: ["js"],
61+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
62+
}
2863
}
2964
],
3065
invalid: [
@@ -212,6 +247,87 @@ tester.run("prh", rule, {
212247
}
213248
]
214249
},
250+
// comment
251+
{
252+
text: `${CODE_START_JS}
253+
// $ is jquery
254+
const $ = jquery;
255+
${CODE_END}`,
256+
output: `${CODE_START_JS}
257+
// $ is jQuery
258+
const $ = jquery;
259+
${CODE_END}`,
260+
errors: [
261+
{
262+
index: 14,
263+
message: "jquery => jQuery"
264+
}
265+
],
266+
options: {
267+
checkCodeComment: ["js"],
268+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
269+
}
270+
},
271+
// BlockComment
272+
{
273+
text: `${CODE_START_JS}
274+
/**
275+
* $ is jquery
276+
**/
277+
const $ = jquery;
278+
${CODE_END}`,
279+
output: `${CODE_START_JS}
280+
/**
281+
* $ is jQuery
282+
**/
283+
const $ = jquery;
284+
${CODE_END}`,
285+
errors: [
286+
{
287+
index: 18,
288+
message: "jquery => jQuery"
289+
}
290+
],
291+
options: {
292+
checkCodeComment: ["js"],
293+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
294+
}
295+
},
296+
// BlockComment multiple
297+
{
298+
text:
299+
"```javascript\n" +
300+
"/**\n" +
301+
" * $ is jquery\n" +
302+
" **/" +
303+
"/**\n" +
304+
" * cookie is Cookie\n" +
305+
" **/\n" +
306+
"```",
307+
output:
308+
"```javascript\n" +
309+
"/**\n" +
310+
" * $ is jQuery\n" +
311+
" **/" +
312+
"/**\n" +
313+
" * Cookie is Cookie\n" +
314+
" **/\n" +
315+
"```",
316+
errors: [
317+
{
318+
index: 26,
319+
message: "jquery => jQuery"
320+
},
321+
{
322+
index: 44,
323+
message: "cookie => Cookie"
324+
}
325+
],
326+
options: {
327+
checkCodeComment: ["javascript"],
328+
rulePaths: [__dirname + "/fixtures/rule.yaml"]
329+
}
330+
},
215331
// example-prh.yml
216332
{
217333
text: "jqueryではクッキー。ディフォルトとハードウエアー。(そのとおり)\nサーバはサーバーサイドをjsする。",

0 commit comments

Comments
 (0)