Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ You can also override or add rules:
| [ui/sentence-case](docs/rules/ui/sentence-case.md) | Enforce sentence case for UI strings | | ✅ ![badge-recommendedWithLocalesEn][] | 🔧 |
| [ui/sentence-case-json](docs/rules/ui/sentence-case-json.md) | Enforce sentence case for English JSON locale strings | | | 🔧 |
| [ui/sentence-case-locale-module](docs/rules/ui/sentence-case-locale-module.md) | Enforce sentence case for English TS/JS locale module strings | | | 🔧 |
| [validate-license](docs/rules/validate-license.md) | Validate the structure of copyright notices in LICENSE files for Obsidian plugins. | | | |
| [validate-manifest](docs/rules/validate-manifest.md) | Validate the structure of manifest.json for Obsidian plugins. | ✅ ![badge-recommendedWithLocalesEn][] | | |
| [vault/iterate](docs/rules/vault/iterate.md) | Avoid iterating all files to find a file by its path<br/> | ✅ ![badge-recommendedWithLocalesEn][] | | 🔧 |

Expand Down
3 changes: 3 additions & 0 deletions docs/rules/validate-license.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Validate the structure of copyright notices in LICENSE files for Obsidian plugins (`obsidianmd/validate-license`)

<!-- end auto-generated rule header -->
17 changes: 17 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import preferFileManagerTrashFile from "./rules/preferFileManagerTrashFile.js";
import regexLookbehind from "./rules/regexLookbehind.js";
import sampleNames from "./rules/sampleNames.js";
import validateManifest from "./rules/validateManifest.js";
import validateLicense from "./rules/validateLicense.js";
import { manifest } from "./readManifest.js";
import { ui } from "./rules/ui/index.js";
import { PlainTextParser } from "./plainTextParser.js";

// --- Import plugins and configs for the recommended config ---
import js from "@eslint/js";
Expand Down Expand Up @@ -57,6 +59,7 @@ const plugin: ESLint.Plugin = {
"regex-lookbehind": regexLookbehind,
"sample-names": sampleNames,
"validate-manifest": validateManifest,
"validate-license": validateLicense,
"ui/sentence-case": ui.sentenceCase,
"ui/sentence-case-json": ui.sentenceCaseJson,
"ui/sentence-case-locale-module": ui.sentenceCaseLocaleModule,
Expand Down Expand Up @@ -188,6 +191,20 @@ const flatRecommendedConfig = [
"obsidianmd/validate-manifest": "error",
},
},
{
// LICENSE validation
plugins: {
obsidianmd: plugin,
},
parserOptions: {
parser: PlainTextParser,
extraFileExtensions: [""],
},
files: ["LICENSE"],
rules: {
"obsidianmd/validate-license": "error",
},
},
] as any;

const hybridRecommendedConfig = {
Expand Down
51 changes: 51 additions & 0 deletions lib/plainTextParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AST_NODE_TYPES, AST_TOKEN_TYPES } from "@typescript-eslint/types";
import type { Parser } from "@typescript-eslint/utils/ts-eslint";

/**
* A plain text parser for ESLint.
* It treats each line as a separate token of type `Line`.
* This allows us to lint plain text files like LICENSE files.
*/
export const PlainTextParser: Parser.ParserModule = {
meta: {
name: "plain-text-parser",
version: "1.0.0",
},
parseForESLint(text: string): Parser.ParseResult {
const lines = text.split("\n");

const tokens: Parser.ParseResult["ast"]["tokens"] = [];

let index = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
tokens.push({
// Line is originally supposed to represent a multi-line comment,
// but we use it to represent a line of text here. The identifier fits best.
type: AST_TOKEN_TYPES.Line,
value: line,
range: [index, index + line.length],
loc: {
start: { line: i + 1, column: 0 },
end: { line: i + 1, column: line.length },
},
});
index += line.length + 1; // +1 for the newline character
}

return {
ast: {
type: AST_NODE_TYPES.Program,
sourceType: "script",
range: [0, text.length],
loc: {
start: tokens[0]?.loc.start ?? { line: 1, column: 0 },
end: tokens[tokens.length - 1].loc.end ?? { line: 1, column: 0 },
},
body: [],
comments: [],
tokens: tokens,
},
};
}
}
87 changes: 87 additions & 0 deletions lib/rules/validateLicense.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { AST_TOKEN_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import path from "path";

const ruleCreator = ESLintUtils.RuleCreator(
(name) =>
`https://github.com/obsidianmd/eslint-plugin/blob/master/docs/rules/${name}.md`,
);

export default ruleCreator({
name: "validate-license",
meta: {
type: "problem" as const,
docs: {
description: "Validate the structure of copyright notices in LICENSE files for Obsidian plugins.",
url: "???",
},
schema: [
{
type: "object",
properties: {
currentYear: {
type: "number",
description: "The current year to validate against.",
},
disableUnchangedYear: {
type: "boolean",
description: "If true, do not report errors for unchanged years.",
default: false,
}
}
}
],
messages: {
unchangedCopyright: "Please change the copyright holder from \"Dynalist Inc.\" to your name.",
unchangedYear: "Please change the copyright year from {{actual}} to the current year ({{expected}}).",
},
},
defaultOptions: [{
currentYear: new Date().getFullYear(),
disableUnchangedYear: false,
}],
create(context, [options]) {
const filename = context.physicalFilename;
if (!path.basename(filename).endsWith("LICENSE")) {
return {};
}

return {
Program(programNode: TSESTree.Program) {
// We want to parse: Copyright (C) 2020-2025 by Dynalist Inc.
// We should check that the year is current and the holder is not "Dynalist Inc."

const copyrightRegex = /^(?: |\t)*Copyright \(C\) (\d{4})(?:-(\d{4}))? by (.+)$/;

// we rely on our plain text parser to give us tokens as Line tokens
for (const token of programNode.tokens ?? []) {
if (token.type !== AST_TOKEN_TYPES.Line) continue;

const match = token.value.match(copyrightRegex);
if (match) {
const startYear = parseInt(match[1], 10);
const endYear = match[2] ? parseInt(match[2], 10) : startYear;
const holder = match[3].trim();

if (!options.disableUnchangedYear && endYear < options.currentYear) {
context.report({
messageId: "unchangedYear",
loc: token.loc,
data: {
expected: options.currentYear.toString(),
actual: endYear.toString(),
}
});
}

if (holder === "Dynalist Inc.") {
context.report({
messageId: "unchangedCopyright",
loc: token.loc,
});
}
}
}
}
};
},
});
1 change: 1 addition & 0 deletions tests/all-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "./noTFileTFolderCast.test";
import "./noViewReferencesInPlugin.test";
import "./noStaticStylesAssignment.test";
import "./validateManifest.test";
import "./validateLicense.test";
import "./noPluginAsComponent.test";
import "./preferAbstractInputSuggest.test";
import "./noSampleCode.test";
Expand Down
108 changes: 108 additions & 0 deletions tests/validateLicense.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import licenseRule from "../lib/rules/validateLicense.js";
import { PlainTextParser } from "lib/plainTextParser.js";

const ruleTester = new RuleTester({
languageOptions: {
parser: PlainTextParser,
parserOptions: {
extraFileExtensions: [""],
}
},

});
const currentYear = new Date().getFullYear();

ruleTester.run("validate-license", licenseRule, {
valid: [
{
filename: "LICENSE",
code: `Copyright (C) 2020-${currentYear} by John Doe`,
},
{
filename: "LICENSE",
code: `Copyright (C) ${currentYear} by John Doe`,
},
{
filename: "LICENSE",
code: `Copyright (C) ${currentYear + 1} by John Doe`,
},
{
filename: "LICENSE",
code: `Copyright (C) 2000 by John Doe`,
options: [{ currentYear: 2000, disableUnchangedYear: false }],
},
{
filename: "LICENSE",
code: `Copyright (C) 2001 by John Doe`,
options: [{ currentYear: 2000, disableUnchangedYear: false }],
},
{
filename: "LICENSE",
code: `Copyright (C) 2001 by John Doe`,
options: [{ currentYear: currentYear, disableUnchangedYear: true }],
},
{
filename: "LICENSE",
code: `foo\nCopyright (C) 2020-${currentYear} by John Doe\nbar`,
},
{
filename: "LICENSE",
code: `foo\nbar\nbaz`,
},
],
invalid: [
{
filename: "LICENSE",
code: `Copyright (C) 2020-${currentYear} by Dynalist Inc.`,
errors: [
{ messageId: "unchangedCopyright" }
],
},
{
filename: "LICENSE",
code: `Copyright (C) 2020-2022 by John Doe`,
errors: [
{ messageId: "unchangedYear", data: { expected: currentYear.toString(), actual: "2022" } }
],
},
{
filename: "LICENSE",
code: `Copyright (C) 2022 by John Doe`,
errors: [
{ messageId: "unchangedYear", data: { expected: currentYear.toString(), actual: "2022" } }
],
},
{
filename: "LICENSE",
code: `Copyright (C) 2020-2022 by Dynalist Inc.`,
errors: [
{ messageId: "unchangedYear", data: { expected: currentYear.toString(), actual: "2022" } },
{ messageId: "unchangedCopyright" }
],
},
{
filename: "LICENSE",
code: `Copyright (C) 2020-2022 by John Doe\nCopyright (C) 2020-${currentYear} by Dynalist Inc.`,
errors: [
{ messageId: "unchangedYear", data: { expected: currentYear.toString(), actual: "2022" } },
{ messageId: "unchangedCopyright" }
],
},
{
filename: "LICENSE",
code: `bar\nCopyright (C) 2020-${currentYear} by Dynalist Inc.\nbaz`,
errors: [
{ messageId: "unchangedCopyright" }
],
},
{
filename: "LICENSE",
code: `Copyright (C) 1999 by John Doe`,
options: [{ currentYear: 2000, disableUnchangedYear: false }],
errors: [
{ messageId: "unchangedYear", data: { expected: "2000", actual: "1999" } }
],
},
],
});
Loading