Skip to content
Draft
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 build/format-changelog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/

/* eslint-disable no-console */

import { readFile, writeFile } from 'node:fs/promises'
Expand Down
1 change: 1 addition & 0 deletions lib/configs/codeStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { ConfigOptions } from '../types.d.ts'

Expand Down
1 change: 1 addition & 0 deletions lib/configs/documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { Settings } from 'eslint-plugin-jsdoc/iterateJsdoc.js'
import type { ConfigOptions } from '../types.d.ts'
Expand Down
17 changes: 17 additions & 0 deletions lib/configs/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { ConfigOptions } from '../types.d.ts'

Expand All @@ -13,6 +14,7 @@ import {
GLOB_FILES_TYPESCRIPT,
GLOB_FILES_VUE,
} from '../globs.ts'
import copyrightPlugin from '../plugins/copyright/index.ts'
import nextcloudPlugin from '../plugins/nextcloud/index.ts'

/**
Expand Down Expand Up @@ -89,6 +91,21 @@ export function javascript(options: ConfigOptions): Linter.Config[] {
}
),

// Copyright header enforcement
{
name: 'nextcloud/javascript/copyright',
files: [
...GLOB_FILES_JAVASCRIPT,
...GLOB_FILES_TYPESCRIPT,
],
plugins: {
'@nextcloud/copyright': copyrightPlugin,
},
rules: {
'@nextcloud/copyright/consistent-copyright-header': 'error',
},
},

// Nextcloud specific overwrite
{
name: 'nextcloud/javascript/rules',
Expand Down
1 change: 1 addition & 0 deletions lib/configs/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { ESLint, Linter } from 'eslint'

import jsonPlugin from '@eslint/json'
Expand Down
1 change: 1 addition & 0 deletions lib/configs/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'

import globals from 'globals'
Expand Down
1 change: 1 addition & 0 deletions lib/configs/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { ConfigOptions } from '../types.d.ts'

Expand Down
1 change: 1 addition & 0 deletions lib/configs/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { ConfigOptions } from '../types.d.ts'

Expand Down
1 change: 1 addition & 0 deletions lib/configs/vue2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { ConfigOptions } from '../types.d.ts'

Expand Down
1 change: 1 addition & 0 deletions lib/configs/vue3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Linter } from 'eslint'
import type { ConfigOptions } from '../types.d.ts'

Expand Down
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { ConfigOptions } from './types.d.ts'

import { codeStyle } from './configs/codeStyle.ts'
Expand Down Expand Up @@ -69,6 +70,7 @@ export const recommendedVue2Library = createConfig({
export { default as packageJsonPlugin } from './plugins/packageJson.ts'
export { default as nextcloudPlugin } from './plugins/nextcloud/index.ts'
export { default as l10nPlugin } from './plugins/l10n/index.ts'
export { default as copyrightPlugin } from './plugins/copyright/index.ts'

/**
* Generate a configuration based on given options
Expand Down
17 changes: 17 additions & 0 deletions lib/plugins/copyright/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { ESLint } from 'eslint'

import { packageVersion } from '../../version.ts'
import { rules } from './rules/index.ts'

export default {
meta: {
name: '@nextcloud/copyright-plugin',
version: packageVersion,
},
rules,
} satisfies ESLint.Plugin
158 changes: 158 additions & 0 deletions lib/plugins/copyright/rules/consistent-copyright-header.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { RuleTester } from 'eslint'
import { describe, it } from 'vitest'
import rule from './consistent-copyright-header.ts'

const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
})

describe('consistent-copyright-header', () => {
it('should pass with correct /*! format and empty line after SPDX header', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [
{
code: `/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

console.log('hello')`,
},
],
invalid: [],
})
})

it('should pass with correct /*! format and empty line after old copyright comment', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [
{
code: `/*!
* Copyright (c) 2025 Nextcloud GmbH
*/

console.log('hello')`,
},
],
invalid: [],
})
})

it('should pass with no comments', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [
{
code: 'console.log("no comments here")',
},
],
invalid: [],
})
})

it('should skip if the first comment is not at the beginning', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [
{
code: `console.log('hello')
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/`,
},
],
invalid: [],
})
})

it('should skip if the first comment is not a copyright comment', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [
{
code: `/**
* A regular JSDoc comment before Foo function
*
* @param bar {any} - Bar
*/
function foo(bar) {}`,
},
],
invalid: [],
})
})

it('should fix JSDoc style /** to /*! format', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [],
invalid: [
{
code: `/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

console.log('hello')`,
errors: [{ messageId: 'wrongCommentFormat', data: { format: '/**' } }],
output: `/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

console.log('hello')`,
},
],
})
})

it('should fix regular /* to /*! format', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [],
invalid: [
{
code: `/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

console.log('hello')`,
errors: [{ messageId: 'wrongCommentFormat', data: { format: '/*' } }],
output: `/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

console.log('hello')`,
},
],
})
})

it('should add an empty line after copyright without a single newline', () => {
ruleTester.run('consistent-copyright-header', rule, {
valid: [],
invalid: [
{
code: `/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
console.log('hello')`,
errors: [{ messageId: 'missingEmptyLine' }],
output: `/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

console.log('hello')`,
},
],
})
})
})
73 changes: 73 additions & 0 deletions lib/plugins/copyright/rules/consistent-copyright-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Rule } from 'eslint'

export default {
meta: {
fixable: 'code',
type: 'layout',
schema: [],
docs: {
description: 'Enforce a copyright header comments to use the legal comment `/*!` format instead of `/**` or `/*`',
},
messages: {
wrongCommentFormat: 'Copyright header must use the legal comment format `/*!`, not {{ format }}',
missingEmptyLine: 'Copyright header must be followed by an empty line',
},
},

create(context) {
const sourceCode = context.sourceCode

return {
Program(node) {
const comments = sourceCode.getAllComments()

// Only check only the very first block comment
if (comments.length === 0 || comments[0].type !== 'Block' || comments[0].range[0] !== 0) {
return
}

// Only check if it's actually a copyright comment
if (!comments[0].value.toLowerCase().includes('copyright')) {
return
}

const comment = comments[0]
const commentText = sourceCode.getText(comment as unknown as Rule.Node)
const commentStart = commentText.match(/^\/\*[^\s]*/)![0]

// Check for correct /*! comment format
if (commentStart !== '/*!') {
context.report({
node,
messageId: 'wrongCommentFormat',
data: { format: commentStart },
fix(fixer) {
return fixer.replaceTextRange(
[comment.range[0], comment.range[0] + commentStart.length],
'/*!',
)
},
})
}

// Check for empty line after copyright comment
const commentEnd = comment.range[1]
const textAfterComment = sourceCode.getText().slice(commentEnd, commentEnd + 2)
if (textAfterComment !== '\n\n') {
context.report({
node,
messageId: 'missingEmptyLine',
fix(fixer) {
return fixer.insertTextAfterRange([commentEnd, commentEnd], '\n')
},
})
}
},
}
},
} satisfies Rule.RuleModule
10 changes: 10 additions & 0 deletions lib/plugins/copyright/rules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import consistentCopyrightHeader from './consistent-copyright-header.ts'

export const rules = {
'consistent-copyright-header': consistentCopyrightHeader,
}
1 change: 1 addition & 0 deletions lib/plugins/l10n/rules/enforce-ellipsis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { RuleTester } from 'eslint'
import { test } from 'vitest'
import rule from './enforce-ellipsis.ts'
Expand Down
Loading
Loading