Skip to content
Open
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
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions packages/concerto-linter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ The linter provide the lintModel function which provides a robust and efficient

- **Custom Ruleset Flexibility**: Enables the use of a custom Spectral ruleset by allowing you to specify its file path for tailored linting rules.

### Compatibility Context

- **Compatibility-aware linting**: Supports `dangerouslyAllowReservedSystemTypeNamesInUserModels` so the linter can warn when reserved system concept names are used in risky compatibility modes.

### Namespace Filtering

- **Namespace Filtering**: Filters linting results by namespace, with configurable exclusion patterns. By default, excludes `'concerto.*'` and `'org.accordproject.*'` namespaces.
Expand Down Expand Up @@ -85,7 +89,7 @@ const ast = modelManager.getAst();
```
## Ruleset Configuration

The concerto Linter provides flexible `ruleset` configuration options to suit your project needs.
The concerto Linter provides flexible configuration options to suit your project needs.

### Default Ruleset

Expand All @@ -111,6 +115,14 @@ You can customize the linting rules in several ways:
const results = await lintModel(ast, {ruleset: 'default'});
```

4. **Enable risky reserved system concept checks for v4 compatibility mode**
```javascript
const results = await lintModel(ast, {
ruleset: 'default',
dangerouslyAllowReservedSystemTypeNamesInUserModels: true
});
```

### Automatic Ruleset Discovery

When you call `lintModel` without specifying a ruleset, the linter will automatically search for ruleset files in the following order:
Expand Down Expand Up @@ -201,7 +213,13 @@ const results = await lintModel(ast, {
ruleset: "D:\\linter-test\\my-ruleset.yaml",
excludeNamespaces: ['org.example.*']
});

// Match concerto-core dangerous compatibility mode during linting
const riskyResults = await lintModel(ast, {
ruleset: 'default',
dangerouslyAllowReservedSystemTypeNamesInUserModels: true
});
```
## License

Accord Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the LICENSE file. Accord Project documentation files are made available under the Creative Commons Attribution 4.0 International License (CC-BY-4.0).
Accord Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the LICENSE file. Accord Project documentation files are made available under the Creative Commons Attribution 4.0 International License (CC-BY-4.0).
13 changes: 12 additions & 1 deletion packages/concerto-linter/default-ruleset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ The following table provides an overview of the available linting rules in the d
<td>Enforces that names used for declarations, properties, and decorators in concerto models do not use reserved keywords. Reserved keywords are language-specific terms that may cause conflicts or unexpected behavior if used as identifiers.</td>
</tr>
<tr>
<td><a href="#reserved-system-concept-declarations">reserved-system-concept-declarations</a></td>
<td>Flags declaration names that collide with reserved Concerto system concepts in legacy/v3 models, or in v4 when dangerous reserved system type names are explicitly enabled for compatibility.</td>
</tr>
<tr>
<td><a href="#pascal-case-declarations">pascal-case-declarations</a></td>
<td>Ensures that declaration names (scalar, enum, concept, asset, participant, transaction, event) follow PascalCase naming convention (e.g., 'MyDeclaration'). This promotes consistency and readability across model declarations.</td>
</tr>
Expand Down Expand Up @@ -101,6 +105,12 @@ To explicitly specify the default ruleset:
const results = await lintModel(modelText, { ruleset: 'default' });
```

The default ruleset includes `reserved-system-concept-declarations`, which behaves as follows:

- v3 and legacy models: reports reserved declaration names such as `Concept`, `Asset`, `Transaction`, `Participant`, and `Event`
- default v4 runs: stays silent
- dangerous v4 compatibility mode: reports the same names when `dangerouslyAllowReservedSystemTypeNamesInUserModels` is enabled through `lintModel`

## Customization

To create your own ruleset that fits your project needs, you can either extend the default ruleset, or create an entirely new ruleset from scratch.
Expand Down Expand Up @@ -147,6 +157,7 @@ Here are all the rule IDs that can be disabled:
|---------|-------------|
| `namespace-version` | Ensures namespaces include version numbers |
| `no-reserved-keywords` | Prevents use of reserved keywords |
| `reserved-system-concept-declarations` | Flags reserved system concept declaration names in risky compatibility contexts |
| `pascal-case-declarations` | Enforces PascalCase for declarations |
| `camel-case-properties` | Enforces camelCase for properties |
| `upper-snake-case-enum-constants` | Enforces UPPER_SNAKE_CASE for enum constants |
Expand Down Expand Up @@ -297,4 +308,4 @@ For more complex rules, you can create custom JavaScript functions following [Sp

## License

Accord Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the LICENSE file. Accord Project documentation files are made available under the Creative Commons Attribution 4.0 International License (CC-BY-4.0).
Accord Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the LICENSE file. Accord Project documentation files are made available under the Creative Commons Attribution 4.0 International License (CC-BY-4.0).
3 changes: 2 additions & 1 deletion packages/concerto-linter/default-ruleset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"@accordproject/concerto-core": "4.1.2",
"@stoplight/spectral-core": "1.20.0",
"@stoplight/spectral-functions": "1.10.1",
"@stoplight/spectral-parsers": "1.0.5"
"@stoplight/spectral-parsers": "1.0.5",
"semver": "7.5.4"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.46.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import semver from 'semver';

const RESERVED_SYSTEM_CONCEPT_NAMES = new Set([
'Concept',
'Asset',
'Transaction',
'Participant',
'Event',
]);

interface Declaration {
name?: string;
}

interface Model {
namespace?: string;
declarations?: Declaration[];
concertoVersion?: string;
}

interface ReservedSystemConceptOptions {
dangerouslyAllowReservedSystemTypeNamesInUserModels?: boolean;
}

function isLegacyOrV3Model(model: Model): boolean {
if (typeof model.concertoVersion === 'string') {
const minimumVersion = semver.minVersion(model.concertoVersion);
if (minimumVersion) {
return minimumVersion.major === 3;
}
}

return typeof model.namespace === 'string' && !model.namespace.includes('@');
}

function getReservedSystemConceptMessage(declarationName: string, isLegacyModel: boolean): string {
if (isLegacyModel) {
return `Declaration '${declarationName}' collides with a reserved Concerto system concept. Rename the declaration or migrate safely before relying on this model.`;
}

return `Declaration '${declarationName}' collides with a reserved Concerto system concept. Rename the declaration, disable dangerouslyAllowReservedSystemTypeNamesInUserModels, or migrate safely before relying on this model.`;
}

/**
* Finds declaration names that collide with reserved Concerto system concepts
* in risky compatibility contexts.
*
* @param {unknown} targetVal The AST node to check, expected to be a Concerto model.
* @param {ReservedSystemConceptOptions} options Rule options controlling v4 dangerous mode behavior.
* @returns {IFunctionResult[]} A result for each matching declaration.
*/
export const findReservedSystemConceptDeclarations: IFunction = (
targetVal,
options?: unknown,
): IFunctionResult[] => {
if (typeof targetVal !== 'object' || targetVal === null) {
throw new Error('Value must be a valid AST object for a Concerto model.');
}

const model = targetVal as Model;
if (!Array.isArray(model.declarations)) {
return [];
}

const functionOptions = options as ReservedSystemConceptOptions | undefined;
const dangerousModeEnabled = Boolean(functionOptions?.dangerouslyAllowReservedSystemTypeNamesInUserModels);
const legacyOrV3Model = isLegacyOrV3Model(model);
const shouldReport = legacyOrV3Model || dangerousModeEnabled;

if (!shouldReport) {
return [];
}

return model.declarations.reduce<IFunctionResult[]>((results, declaration) => {
if (!declaration?.name || !RESERVED_SYSTEM_CONCEPT_NAMES.has(declaration.name)) {
return results;
}

results.push({
message: getReservedSystemConceptMessage(declaration.name, legacyOrV3Model),
});
Comment thread
Rishabh060105 marked this conversation as resolved.

return results;
}, []);
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default {
then: {
function: pattern,
functionOptions: {
notMatch: '/^(String|Double|Integer|Long|DateTime|Boolean|scalar|concept|enum|asset|participant|transaction|event|map|optional|length|regex|range|default)$/i'
notMatch: '/^(String|Double|Integer|Long|DateTime|Boolean|scalar|concept|enum|asset|participant|transaction|event|map|optional|length|regex|range|default)$/'
Comment thread
Rishabh060105 marked this conversation as resolved.
},
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { findReservedSystemConceptDeclarations } from './functions/find-reserved-system-concept-declarations';

/**
* Rule: Reserved System Concept Declarations
* ------------------------------------------
* Flags declaration names that collide with reserved Concerto system concepts
* in legacy/v3 models, or when dangerous reserved system type names are
* explicitly allowed in v4 compatibility mode.
*/
export default {
given: '$.models[*]',
severity: 0, // 0 = error, 1 = warning, 2 = info, 3 = hint
then: {
function: findReservedSystemConceptDeclarations,
},
};
2 changes: 2 additions & 0 deletions packages/concerto-linter/default-ruleset/src/ruleset-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import stringLengthValidator from './string-length-validator';
import noReservedKeywords from './no-reserved-keywords';
import noEmptyDeclarations from './no-empty-declarations';
import abstractMustSubclassed from './abstract-must-subclassed';
import reservedSystemConceptDeclarations from './reserved-system-concept-declarations';

const concertoRuleset: RulesetDefinition = {
rules: {
Expand All @@ -35,6 +36,7 @@ const concertoRuleset: RulesetDefinition = {
'string-length-validator': stringLengthValidator,
'no-empty-declarations': noEmptyDeclarations,
'abstract-must-subclassed': abstractMustSubclassed,
'reserved-system-concept-declarations': reservedSystemConceptDeclarations,
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace org.example@1.0.0

asset Asset identified by assetId {
o String assetId
}

transaction Transaction {
o String transactionId optional
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace org.example

participant Participant identified by participantId {
o String participantId
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
concerto version "^3.0.0"

namespace org.example@1.0.0

asset Asset identified by assetId {
o String assetId
}

concept Concept {
o String name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace org.example@1.0.0

asset Asset identified by assetId {
o String assetId
}

event Event {
o String eventId optional
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace org.example@1.0.0

asset Vehicle identified by vehicleId {
o String vehicleId
}

concept Shipment {
o String shipmentId
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ describe('No Reserved Keywords Rule', () => {
const messageText = results.map(r => r.message).join(' ');
expect(messageText).toContain('is a reserved keyword');
});

test('should not report reserved system concept declarations through the generic keyword rule', async () => {
const results = await testRules({
rules: {
'no-reserved-keywords': noReservedKeywords,
}
}, 'no-reserved-keywords-system-concepts-valid.cto');

expect(results).toHaveLength(0);
});
});
Loading
Loading