Skip to content

Commit c705f9f

Browse files
authored
Add scrubber POC (#3)
* Remove redundant prettier write * Create scrubber tool * Add tag opening and closing braces and allow scrub on directories * Add test dir * Add another test file * Fix carriage return line ending issue and restore prod script * Require tags in comments
1 parent 8501680 commit c705f9f

File tree

10 files changed

+240
-2
lines changed

10 files changed

+240
-2
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ module.exports = {
1414
],
1515
rules: {
1616
"no-console": "off",
17+
"no-plusplus": "off",
18+
"no-continue": "off",
1719
},
1820
};

index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
11
import cli from "./cli";
22

3+
import Scrubber from "./scrubber/scrubber";
4+
import { ScrubberAction } from "./scrubber/scrubberTypes";
5+
6+
async function scrub() {
7+
try {
8+
const actions: ScrubberAction[] = [{ type: "remove", tags: ["@remove"] }];
9+
const scrubber = new Scrubber();
10+
11+
await scrubber.parseConfig("scrubber/scrubberConfig.json");
12+
await scrubber.start(actions);
13+
} catch (err) {
14+
console.log(err);
15+
}
16+
}
17+
318
cli(process.argv);
19+
scrub();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"scripts": {
1010
"dev": "ts-node index.ts",
1111
"lint": "eslint . --ext .ts,.js",
12-
"lint-fix": "eslint . --ext .ts,.js --fix && prettier --write **/*.ts **/*.js",
12+
"lint-fix": "eslint . --ext .ts,.js --fix",
1313
"prod": "tsc -p . && node bin/index.js"
1414
},
1515
"devDependencies": {

scrubber/scrubber.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import {
4+
ScrubberAction,
5+
TagNameToAction,
6+
ScrubberConfig,
7+
} from "./scrubberTypes";
8+
9+
const TAG_START_CHAR = "{";
10+
const TAG_END_CHAR = "}";
11+
12+
const FILE_TYPE_COMMENT: { [key: string]: string } = {
13+
js: "//",
14+
json: "//",
15+
ts: "//",
16+
py: "#",
17+
};
18+
19+
function scrubberActionsToDict(actions: ScrubberAction[]): TagNameToAction {
20+
const dict: TagNameToAction = {};
21+
actions.forEach((action) => {
22+
action.tags.forEach((tag: string) => {
23+
dict[tag] = action.type;
24+
});
25+
});
26+
return dict;
27+
}
28+
29+
async function getConfigFile(filename: string): Promise<ScrubberConfig> {
30+
try {
31+
const configString = await fs.readFileSync(filename, "utf8");
32+
return JSON.parse(configString);
33+
} catch (err) {
34+
console.error("Failed to read file ", filename);
35+
throw err;
36+
}
37+
}
38+
39+
async function scrubFile(
40+
filePath: string,
41+
tags: TagNameToAction,
42+
isDryRun: boolean,
43+
): Promise<void> {
44+
return new Promise((resolve, reject) => {
45+
fs.readFile(filePath, { encoding: "utf8" }, async (err, text) => {
46+
if (err) {
47+
reject(err);
48+
}
49+
50+
const ext = filePath.split(".").pop();
51+
const commentType = ext && FILE_TYPE_COMMENT[ext];
52+
const scrubbedLines: string[] = [];
53+
let skip = false;
54+
55+
const lines: string[] = text.split("\n");
56+
57+
for (let i = 0; i < lines.length; ++i) {
58+
const line = lines[i];
59+
let tryProcessTag = true;
60+
61+
if (line.length === 0) {
62+
scrubbedLines.push(line);
63+
continue;
64+
}
65+
66+
// Split on whitespace
67+
const tokens = line.trim().split(/[ ]+/);
68+
69+
if (commentType) {
70+
if (tokens[0] !== commentType) {
71+
tryProcessTag = false;
72+
}
73+
tokens.shift();
74+
}
75+
76+
if (tryProcessTag) {
77+
if (tokens[0] in tags && tokens.length !== 2) {
78+
console.warn(
79+
`WARNING line ${
80+
i + 1
81+
}: possible malformed tag; tags must be on their own line preceded by '}' or followed by '{'`,
82+
);
83+
scrubbedLines.push(line);
84+
continue;
85+
}
86+
87+
if (tokens[0] in tags || tokens[1] in tags) {
88+
const tag = tokens[0] in tags ? tokens[0] : tokens[1];
89+
const brace = tag === tokens[0] ? tokens[1] : tokens[0];
90+
91+
if (brace === tokens[1] && brace !== TAG_START_CHAR) {
92+
throw new Error(
93+
`Malformed tag ${filePath}:line ${
94+
i + 1
95+
}: expected '{' after tag name`,
96+
);
97+
}
98+
99+
if (brace === tokens[0] && brace !== TAG_END_CHAR) {
100+
throw new Error(
101+
`Malformed tag ${filePath}:line ${
102+
i + 1
103+
}: expected '}' before tag name`,
104+
);
105+
}
106+
107+
// NOTE: nested tagging is not currently expected and will lead to unexpected behaviour.
108+
109+
if (tags[tag] === "remove") {
110+
skip = brace === TAG_START_CHAR;
111+
}
112+
113+
// We always scrub tags from the final file.
114+
continue;
115+
}
116+
}
117+
118+
if (skip) {
119+
if (isDryRun) {
120+
console.log(`Skipping line ${i + 1}`);
121+
}
122+
continue;
123+
}
124+
125+
scrubbedLines.push(line);
126+
}
127+
128+
if (isDryRun) return;
129+
130+
fs.writeFileSync(filePath, scrubbedLines.join("\n"));
131+
132+
resolve();
133+
});
134+
});
135+
}
136+
137+
async function scrubDir(dir: string, tags: TagNameToAction, isDryRun: boolean) {
138+
const files = await fs.readdirSync(dir);
139+
const promises = files.map(
140+
async (name: string): Promise<void> => {
141+
const filePath = path.join(dir, name);
142+
const stat = fs.statSync(filePath);
143+
if (stat.isFile()) {
144+
return scrubFile(filePath, tags, isDryRun);
145+
}
146+
if (stat.isDirectory()) {
147+
return scrubDir(filePath, tags, isDryRun);
148+
}
149+
return Promise.resolve();
150+
},
151+
);
152+
await Promise.all(promises);
153+
}
154+
155+
class Scrubber {
156+
tags: TagNameToAction = {};
157+
158+
dirs: string[] = [];
159+
160+
async parseConfig(filename: string): Promise<void> {
161+
// TODO validate config (e.g.properly formed tag names)
162+
const config = await getConfigFile(filename);
163+
this.tags = scrubberActionsToDict(config.actions);
164+
this.dirs = config.dirs;
165+
}
166+
167+
// Scrub files
168+
async start(
169+
actions: ScrubberAction[],
170+
isDryRun: boolean = false,
171+
): Promise<void> {
172+
const tags = { ...this.tags, ...scrubberActionsToDict(actions) };
173+
174+
// TODO: specify file extensions?
175+
await Promise.all(this.dirs.map((dir) => scrubDir(dir, tags, isDryRun)));
176+
}
177+
}
178+
179+
export default Scrubber;

scrubber/scrubberConfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"actions": [
3+
{
4+
"type": "keep",
5+
"tags": ["@remove", "@remove2"]
6+
}
7+
],
8+
"dirs": ["test_dir"]
9+
}

scrubber/scrubberTypes.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
Types required by the scrubber tool.
3+
*/
4+
5+
export type ScrubberActionType = "remove" | "keep";
6+
7+
export type ScrubberAction = {
8+
type: ScrubberActionType;
9+
tags: string[];
10+
};
11+
12+
export type ScrubberConfig = {
13+
actions: ScrubberAction[];
14+
dirs: string[];
15+
};
16+
17+
export type TagNameToAction = { [key: string]: ScrubberActionType };

test_dir/test

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@remove {
2+
this should be gone
3+
} @remove
4+
5+
hello world

test_dir/test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// @remove {
2+
console.log("this should be gone");
3+
// } @remove
4+
5+
console.log("hello world");

test_dir/test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @remove {
2+
print('this should be gone')
3+
# } @remove
4+
5+
print('hello world')

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
/* Basic Options */
66
// "incremental": true, /* Enable incremental compilation */
7-
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
7+
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
88
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
99
// "lib": [], /* Specify library files to be included in the compilation. */
1010
// "allowJs": true, /* Allow javascript files to be compiled. */

0 commit comments

Comments
 (0)