@puruslang/linter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ <div align="center">
2
+
3
+ [![Logo](./logo.png)](https://purus.work)
4
+
5
+ **English** | [日本語](./README-ja.md)
6
+
7
+ </div>
8
+
9
+ ---
10
+
11
+ [![npm](https://img.shields.io/npm/v/purus)](https://www.npmjs.com/package/purus)
12
+ [![npm downloads](https://img.shields.io/npm/dm/purus)](https://www.npmjs.com/package/purus)
13
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/otoneko1102/purus)](https://github.com/otoneko1102/purus/pulse)
14
+ [![GitHub last commit](https://img.shields.io/github/last-commit/otoneko1102/purus)](https://github.com/otoneko1102/purus/commits/main)
15
+ ![US layout](https://img.shields.io/badge/US_layout-Supported-green)
16
+ ![JIS layout](https://img.shields.io/badge/JIS_layout-Supported-green)
17
+
18
+ Purus - _/ˈpuː.rus/_ _**means pure✨ in Latin**_ - is a beautiful, simple, and easy-to-use language. It compiles to _JavaScript_.
19
+
20
+ **It makes your fingers free from the _Shift key_.**
21
+
22
+ With Purus, you can write code almost without pressing the _Shift key_.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ # Global
28
+ npm install -g purus
29
+
30
+ # or Local
31
+ npm install -D purus
32
+ ```
33
+
34
+ ## File Extensions
35
+
36
+ | Extension | Output |
37
+ | --------- | ------ |
38
+ | `.purus` | `.js` |
39
+ | `.cpurus` | `.cjs` |
40
+ | `.mpurus` | `.mjs` |
41
+
42
+ ## Tooling
43
+
44
+ - **VS Code Extension** — [Marketplace](https://marketplace.visualstudio.com/items?itemName=otoneko1102.purus): Syntax highlighting, snippets, file icons
45
+ - **Linter** — [`@puruslang/linter`](https://www.npmjs.com/package/@puruslang/linter): Static analysis for Purus
46
+ - **Prettier Plugin** — [`@puruslang/prettier-plugin-purus`](https://www.npmjs.com/package/@puruslang/prettier-plugin-purus): Code formatting
47
+
48
+ ## Documentation
49
+
50
+ The documentation is available on [purus.work](https://purus.work).
51
+
52
+ ## Author
53
+
54
+ otoneko. https://github.com/otoneko1102
55
+
56
+ ## License
57
+
58
+ Distributed under the Apache 2.0 License. See [LICENSE](./LICENSE) for more information.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@puruslang/linter",
3
+ "version": "0.0.1",
4
+ "description": "Linter for the Purus language",
5
+ "license": "Apache-2.0",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "purus-lint": "src/cli.js"
9
+ },
10
+ "keywords": [
11
+ "linter",
12
+ "purus",
13
+ "static-analysis",
14
+ "altjs",
15
+ "lang",
16
+ "language",
17
+ "noshift",
18
+ "noshift.js"
19
+ ],
20
+ "author": "otoneko1102 (https://github.com/otoneko1102)",
21
+ "homepage": "https://purus.work",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/otoneko1102/purus",
25
+ "directory": "linter"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/otoneko1102/purus/issues"
29
+ },
30
+ "funding": {
31
+ "url": "https://github.com/sponsors/otoneko1102"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ }
36
+ }
package/src/cli.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { lint, defaultRules } = require("./index.js");
7
+
8
+ const args = process.argv.slice(2);
9
+
10
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
11
+ console.log("purus-lint - Linter for the Purus language");
12
+ console.log("");
13
+ console.log("Usage: purus-lint <file.purus|.cpurus|.mpurus> [options]");
14
+ console.log("");
15
+ console.log("Options:");
16
+ console.log(" --config <file> Path to config JSON file");
17
+ console.log(" --fix (not yet implemented)");
18
+ console.log(" --help Show this help");
19
+ process.exit(0);
20
+ }
21
+
22
+ // Collect files and options
23
+ let configPath = null;
24
+ const files = [];
25
+
26
+ for (let i = 0; i < args.length; i++) {
27
+ if (args[i] === "--config" && i + 1 < args.length) {
28
+ configPath = args[++i];
29
+ } else if (!args[i].startsWith("-")) {
30
+ files.push(args[i]);
31
+ }
32
+ }
33
+
34
+ // Load config
35
+ let ruleOverrides = {};
36
+ if (configPath) {
37
+ try {
38
+ ruleOverrides = JSON.parse(fs.readFileSync(configPath, "utf8"));
39
+ } catch (err) {
40
+ console.error(`Error reading config: ${err.message}`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ // Also check for .puruslint.json in cwd
46
+ if (!configPath) {
47
+ const defaultConfig = path.join(process.cwd(), ".puruslint.json");
48
+ if (fs.existsSync(defaultConfig)) {
49
+ try {
50
+ ruleOverrides = JSON.parse(fs.readFileSync(defaultConfig, "utf8"));
51
+ } catch {
52
+ // ignore
53
+ }
54
+ }
55
+ }
56
+
57
+ let totalIssues = 0;
58
+
59
+ for (const file of files) {
60
+ let source;
61
+ try {
62
+ source = fs.readFileSync(file, "utf8");
63
+ } catch (err) {
64
+ console.error(`Error: ${err.message}`);
65
+ continue;
66
+ }
67
+
68
+ const diagnostics = lint(source, ruleOverrides);
69
+ totalIssues += diagnostics.length;
70
+
71
+ for (const d of diagnostics) {
72
+ const icon = d.severity === "error" ? "error" : "warn";
73
+ console.log(`${file}:${d.line}:${d.col} ${icon} ${d.message} (${d.rule})`);
74
+ }
75
+ }
76
+
77
+ if (totalIssues > 0) {
78
+ console.log(`\n${totalIssues} issue${totalIssues === 1 ? "" : "s"} found.`);
79
+ process.exit(1);
80
+ } else if (files.length > 0) {
81
+ console.log("No issues found.");
82
+ }
package/src/index.js ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+
3
+ const KEYWORDS = new Set([
4
+ "const", "let", "var", "be",
5
+ "fn", "async", "return", "to", "gives",
6
+ "if", "elif", "else", "unless", "then",
7
+ "while", "until", "for", "in", "range",
8
+ "match", "when",
9
+ "try", "catch", "finally", "throw",
10
+ "import", "from", "export", "default", "require", "use", "mod", "pub", "all",
11
+ "add", "sub", "mul", "div", "mod", "neg",
12
+ "eq", "ne", "lt", "gt", "le", "ge",
13
+ "and", "or", "not", "pipe",
14
+ "is", "as", "of", "typeof", "instanceof", "type",
15
+ "new", "delete", "this", "await",
16
+ "true", "false", "null", "nil", "undefined",
17
+ "break", "continue",
18
+ "list", "object",
19
+ ]);
20
+
21
+ function tokenize(source) {
22
+ const tokens = [];
23
+ let i = 0;
24
+ let line = 1;
25
+ let col = 1;
26
+ const len = source.length;
27
+
28
+ while (i < len) {
29
+ const startLine = line;
30
+ const startCol = col;
31
+
32
+ // Newline
33
+ if (source[i] === "\n") {
34
+ tokens.push({ type: "newline", value: "\n", line: startLine, col: startCol });
35
+ i++; line++; col = 1;
36
+ continue;
37
+ }
38
+ if (source[i] === "\r") { i++; continue; }
39
+
40
+ // Whitespace
41
+ if (source[i] === " " || source[i] === "\t") {
42
+ let start = i;
43
+ while (i < len && (source[i] === " " || source[i] === "\t")) { i++; col++; }
44
+ tokens.push({ type: "whitespace", value: source.slice(start, i), line: startLine, col: startCol });
45
+ continue;
46
+ }
47
+
48
+ // Block comment ---
49
+ if (source[i] === "-" && source[i + 1] === "-" && source[i + 2] === "-") {
50
+ let end = source.indexOf("---", i + 3);
51
+ if (end === -1) end = len; else end += 3;
52
+ const val = source.slice(i, end);
53
+ for (const ch of val) { if (ch === "\n") { line++; col = 1; } else { col++; } }
54
+ tokens.push({ type: "block-comment", value: val, line: startLine, col: startCol });
55
+ i = end;
56
+ continue;
57
+ }
58
+
59
+ // Line comment --
60
+ if (source[i] === "-" && source[i + 1] === "-") {
61
+ let end = source.indexOf("\n", i);
62
+ if (end === -1) end = len;
63
+ tokens.push({ type: "comment", value: source.slice(i, end), line: startLine, col: startCol });
64
+ col += end - i; i = end;
65
+ continue;
66
+ }
67
+
68
+ // String ///
69
+ if (source[i] === "/" && source[i + 1] === "/" && source[i + 2] === "/") {
70
+ let j = i + 3; col += 3;
71
+ while (j < len) {
72
+ if (source[j] === "\\" && j + 1 < len) { j += 2; col += 2; continue; }
73
+ if (source[j] === "/" && source[j + 1] === "/" && source[j + 2] === "/") { j += 3; col += 3; break; }
74
+ if (source[j] === "\n") { line++; col = 1; } else { col++; }
75
+ j++;
76
+ }
77
+ tokens.push({ type: "string", value: source.slice(i, j), line: startLine, col: startCol });
78
+ i = j;
79
+ continue;
80
+ }
81
+
82
+ // Punctuation
83
+ if ("[],;.".includes(source[i])) {
84
+ tokens.push({ type: "punct", value: source[i], line: startLine, col: startCol });
85
+ i++; col++;
86
+ continue;
87
+ }
88
+
89
+ // Word
90
+ if (/[a-zA-Z]/.test(source[i])) {
91
+ let start = i;
92
+ while (i < len && /[a-zA-Z0-9-]/.test(source[i])) { i++; col++; }
93
+ const word = source.slice(start, i);
94
+ tokens.push({ type: KEYWORDS.has(word) ? "keyword" : "ident", value: word, line: startLine, col: startCol });
95
+ continue;
96
+ }
97
+
98
+ // Number
99
+ if (/[0-9]/.test(source[i])) {
100
+ let start = i;
101
+ while (i < len && /[0-9]/.test(source[i])) { i++; col++; }
102
+ if (i < len && source[i] === "." && i + 1 < len && /[0-9]/.test(source[i + 1])) {
103
+ i++; col++;
104
+ while (i < len && /[0-9]/.test(source[i])) { i++; col++; }
105
+ }
106
+ tokens.push({ type: "number", value: source.slice(start, i), line: startLine, col: startCol });
107
+ continue;
108
+ }
109
+
110
+ // Shebang
111
+ if (i === 0 && source[i] === "#" && source[i + 1] === "!") {
112
+ let end = source.indexOf("\n", i);
113
+ if (end === -1) end = len;
114
+ tokens.push({ type: "shebang", value: source.slice(i, end), line: startLine, col: startCol });
115
+ col += end - i; i = end;
116
+ continue;
117
+ }
118
+
119
+ // Other
120
+ tokens.push({ type: "other", value: source[i], line: startLine, col: startCol });
121
+ i++; col++;
122
+ }
123
+ return tokens;
124
+ }
125
+
126
+ // --- Rules ---
127
+
128
+ const defaultRules = {
129
+ "no-var": { severity: "warn", message: "Avoid 'var'; use 'const' or 'let' instead" },
130
+ "no-nil": { severity: "warn", message: "Use 'null' instead of 'nil'" },
131
+ "indent-size": { severity: "warn", size: 2 },
132
+ "no-trailing-whitespace": { severity: "warn", message: "Trailing whitespace" },
133
+ "no-unused-import": { severity: "warn" },
134
+ "consistent-naming": { severity: "warn", style: "kebab-case" },
135
+ "max-line-length": { severity: "off", max: 100 },
136
+ };
137
+
138
+ function lint(source, ruleOverrides = {}) {
139
+ const rules = { ...defaultRules, ...ruleOverrides };
140
+ const diagnostics = [];
141
+ const tokens = tokenize(source);
142
+ const lines = source.split("\n");
143
+
144
+ function report(rule, line, col, message) {
145
+ const sev = rules[rule]?.severity || "warn";
146
+ if (sev === "off") return;
147
+ diagnostics.push({ rule, severity: sev, line, col, message });
148
+ }
149
+
150
+ // --- Token-level rules ---
151
+ for (let i = 0; i < tokens.length; i++) {
152
+ const tok = tokens[i];
153
+
154
+ // no-var
155
+ if (rules["no-var"]?.severity !== "off" && tok.type === "keyword" && tok.value === "var") {
156
+ report("no-var", tok.line, tok.col, rules["no-var"].message);
157
+ }
158
+
159
+ // no-nil
160
+ if (rules["no-nil"]?.severity !== "off" && tok.type === "keyword" && tok.value === "nil") {
161
+ report("no-nil", tok.line, tok.col, rules["no-nil"].message);
162
+ }
163
+
164
+ // consistent-naming
165
+ if (rules["consistent-naming"]?.severity !== "off" && tok.type === "ident") {
166
+ const style = rules["consistent-naming"].style || "kebab-case";
167
+ if (style === "kebab-case") {
168
+ // Identifiers should be kebab-case (lowercase with hyphens)
169
+ // Allow PascalCase for class names (starts with uppercase)
170
+ if (/[A-Z]/.test(tok.value[0])) continue; // Allow PascalCase
171
+ if (/_/.test(tok.value)) {
172
+ report("consistent-naming", tok.line, tok.col,
173
+ `Use kebab-case instead of snake_case: '${tok.value}'`);
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // --- Line-level rules ---
180
+ for (let li = 0; li < lines.length; li++) {
181
+ const line = lines[li];
182
+ const lineNum = li + 1;
183
+
184
+ // no-trailing-whitespace
185
+ if (rules["no-trailing-whitespace"]?.severity !== "off") {
186
+ if (line.length > 0 && /\s+$/.test(line) && line.trim().length > 0) {
187
+ report("no-trailing-whitespace", lineNum, line.length,
188
+ rules["no-trailing-whitespace"].message || "Trailing whitespace");
189
+ }
190
+ }
191
+
192
+ // indent-size
193
+ if (rules["indent-size"]?.severity !== "off") {
194
+ const match = line.match(/^( +)/);
195
+ if (match) {
196
+ const size = rules["indent-size"].size || 2;
197
+ if (match[1].length % size !== 0) {
198
+ report("indent-size", lineNum, 1,
199
+ `Indentation should be a multiple of ${size} spaces (found ${match[1].length})`);
200
+ }
201
+ }
202
+ // Warn on tabs if indent style is spaces
203
+ if (/^\t/.test(line)) {
204
+ report("indent-size", lineNum, 1, "Use spaces for indentation, not tabs");
205
+ }
206
+ }
207
+
208
+ // max-line-length
209
+ if (rules["max-line-length"]?.severity !== "off") {
210
+ const max = rules["max-line-length"].max || 100;
211
+ if (line.length > max) {
212
+ report("max-line-length", lineNum, max + 1,
213
+ `Line exceeds max length of ${max} (found ${line.length})`);
214
+ }
215
+ }
216
+ }
217
+
218
+ // Sort by line, then column
219
+ diagnostics.sort((a, b) => a.line - b.line || a.col - b.col);
220
+ return diagnostics;
221
+ }
222
+
223
+ module.exports = { lint, tokenize, defaultRules };