Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Ignore non-Minecraft JSON files #1758

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
8 changes: 7 additions & 1 deletion packages/core/src/service/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Uri } from '../common/index.js'
import type { PosRangeLanguageError } from '../source/index.js'
import type { UnlinkedSymbolTable } from '../symbol/index.js'
import { SymbolTable } from '../symbol/index.js'
import { ArchiveUriSupporter } from './FileService.js'
import type { RootUriString } from './fileUtil.js'
import { fileUtil } from './fileUtil.js'
import type { Project } from './Project.js'
Expand Down Expand Up @@ -66,7 +67,12 @@ export class CacheService {
*/
constructor(private readonly cacheRoot: RootUriString, private readonly project: Project) {
this.project.on('documentUpdated', async ({ doc }) => {
if (!this.#hasValidatedFiles) {
if (
!this.#hasValidatedFiles
// Do not save checksums for file schemes that we cannot map to disk (e.g. 'untitled:'
// for untitled files in VS Code)
|| !(doc.uri.startsWith(ArchiveUriSupporter.Protocol) || doc.uri.startsWith('file:'))
) {
return
}
try {
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/service/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,10 @@ export namespace UriBinderContext {
return { ...ContextBase.create(project), config: project.config, symbols: project.symbols }
}
}

export interface UriPredicateContext extends ContextBase {}
export namespace UriPredicateContext {
export function create(project: ProjectData): UriPredicateContext {
return { ...ContextBase.create(project) }
}
}
10 changes: 6 additions & 4 deletions packages/core/src/service/FileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,12 +290,14 @@ export class ArchiveUriSupporter implements UriProtocolSupporter {
}

*listFiles() {
for (const [archiveName, files] of this.entries.entries()) {
for (const [archiveName, entries] of this.entries.entries()) {
this.logger.info(
`[ArchiveUriSupporter#listFiles] Listing ${files.size} files from ${archiveName}`,
`[ArchiveUriSupporter#listFiles] Listing ${entries.size} entries from ${archiveName}`,
)
for (const file of files.values()) {
yield ArchiveUriSupporter.getUri(archiveName, file.path)
for (const entry of entries.values()) {
if (entry.type === 'file') {
yield ArchiveUriSupporter.getUri(archiveName, entry.path)
}
}
}
}
Expand Down
19 changes: 10 additions & 9 deletions packages/core/src/service/MetaRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../processor/index.js'
import type { Linter } from '../processor/linter/Linter.js'
import type { SignatureHelpProvider } from '../processor/SignatureHelpProvider.js'
import type { UriPredicateContext } from '../service/index.js'
import type { DependencyKey, DependencyProvider } from './Dependency.js'
import type { FileExtension } from './fileUtil.js'
import type { SymbolRegistrar } from './SymbolRegistrar.js'
Expand All @@ -33,11 +34,18 @@ export interface LanguageOptions {
* An array of extensions of files corresponding to the language. Each extension should include the leading dot (`.`).
*/
extensions: FileExtension[]
/**
* If specified, the URI of the file must pass the predicate for it to be considered to be a file
* of this language.
*/
uriPredicate?: UriPredicate
triggerCharacters?: string[]
parser?: Parser<AstNode>
completer?: Completer<any>
}

export type UriPredicate = (uri: string, ctx: UriPredicateContext) => boolean

interface LinterRegistration {
configValidator: (ruleName: string, ruleValue: unknown, logger: Logger) => unknown
linter: Linter<AstNode>
Expand Down Expand Up @@ -106,15 +114,8 @@ export class MetaRegistry {
return Array.from(this.#languages.keys())
}

public isSupportedLanguage(language: string): boolean {
return this.#languages.has(language)
}

/**
* An array of file extensions (including the leading dot (`.`)) that are supported.
*/
public getSupportedFileExtensions(): FileExtension[] {
return [...this.#languages.values()].flatMap((v) => v.extensions)
public getLanguageOptions(language: string): LanguageOptions | undefined {
return this.#languages.get(language)
}

/**
Expand Down
75 changes: 50 additions & 25 deletions packages/core/src/service/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
LinterContext,
ParserContext,
UriBinderContext,
UriPredicateContext,
} from './Context.js'
import type { Dependency } from './Dependency.js'
import { DependencyKey } from './Dependency.js'
Expand Down Expand Up @@ -287,10 +288,7 @@ export class Project implements ExternalEventEmitter {
* are not loaded into the memory.
*/
getTrackedFiles(): string[] {
const extensions: string[] = this.meta.getSupportedFileExtensions()
this.logger.info(`[Project#getTrackedFiles] Supported file extensions: ${extensions}`)
const supportedFiles = [...this.#dependencyFiles ?? [], ...this.#watchedFiles]
.filter((file) => extensions.includes(fileUtil.extname(file) ?? ''))
this.logger.info(
`[Project#getTrackedFiles] Listed ${supportedFiles.length} supported files`,
Comment on lines 290 to 293
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extension check here has been removed as all files in #dependencyFiles and #watchedFiles should now all be of supported languages already.

)
Expand Down Expand Up @@ -525,7 +523,8 @@ export class Project implements ExternalEventEmitter {

await Promise.all([listDependencyFiles(), listProjectFiles()])

this.#dependencyFiles = new Set(this.fs.listFiles())
this.#dependencyFiles = new Set([...this.fs.listFiles()]
.filter((uri) => !this.shouldExclude(uri)))
this.#dependencyRoots = new Set(this.fs.listRoots())

this.updateRoots()
Expand Down Expand Up @@ -659,13 +658,9 @@ export class Project implements ExternalEventEmitter {
this.#textDocumentCache.delete(uri)
}
private async read(uri: string): Promise<TextDocument | undefined> {
const getLanguageID = (uri: string): string => {
const ext = fileUtil.extname(uri) ?? '.plaintext'
return this.meta.getLanguageID(ext) ?? ext.slice(1)
}
const createTextDocument = async (uri: string): Promise<TextDocument | undefined> => {
const languageId = getLanguageID(uri)
if (!this.meta.isSupportedLanguage(languageId)) {
const languageId = this.guessLanguageID(uri)
if (!this.isSupportedLanguage(uri, languageId)) {
return undefined
}

Expand Down Expand Up @@ -872,10 +867,10 @@ export class Project implements ExternalEventEmitter {
content: string,
): Promise<void> {
uri = this.normalizeUri(uri)
if (!fileUtil.isFileUri(uri)) {
return // We only accept `file:` scheme for client-managed URIs.
if (uri.startsWith(ArchiveUriSupporter.Protocol)) {
return // We do not accept `archive:` scheme for client-managed URIs.
}
if (this.shouldExclude(uri)) {
if (this.shouldExclude(uri, languageID)) {
return
}
const doc = TextDocument.create(uri, languageID, version, content)
Expand All @@ -899,17 +894,16 @@ export class Project implements ExternalEventEmitter {
): Promise<void> {
uri = this.normalizeUri(uri)
this.#symbolUpToDateUris.delete(uri)
if (!fileUtil.isFileUri(uri)) {
return // We only accept `file:` scheme for client-managed URIs.
}
if (this.shouldExclude(uri)) {
return
if (uri.startsWith(ArchiveUriSupporter.Protocol)) {
return // We do not accept `archive:` scheme for client-managed URIs.
}
const doc = this.#clientManagedDocAndNodes.get(uri)?.doc
if (!doc) {
throw new Error(
`TextDocument for ${uri} is not cached. This should not happen. Did the language client send a didChange notification without sending a didOpen one, or is there a logic error on our side resulting the 'read' function overriding the 'TextDocument' created in the 'didOpen' notification handler?`,
)
if (!doc || this.shouldExclude(uri, doc.languageId)) {
// If doc is undefined, it means the document was previously excluded by onDidOpen()
// based on the language ID supplied by the client, in which case we should return early.
// Otherwise, we perform the shouldExclude() check with the URI and the saved language ID
// as usual.
return
}
TextDocument.update(doc, changes, version)
const node = this.parse(doc)
Expand All @@ -925,8 +919,8 @@ export class Project implements ExternalEventEmitter {
*/
onDidClose(uri: string): void {
uri = this.normalizeUri(uri)
if (!fileUtil.isFileUri(uri)) {
return // We only accept `file:` scheme for client-managed URIs.
if (uri.startsWith(ArchiveUriSupporter.Protocol)) {
return // We do not accept `archive:` scheme for client-managed URIs.
}
this.#clientManagedUris.delete(uri)
this.#clientManagedDocAndNodes.delete(uri)
Expand Down Expand Up @@ -967,7 +961,38 @@ export class Project implements ExternalEventEmitter {
}
}

public shouldExclude(uri: string): boolean {
/**
* Returns true iff the URI should be excluded from all Spyglass language support.
*
* @param language Optional. If ommitted, a language will be derived from the URI according to
* its file extension.
*/
public shouldExclude(uri: string, language?: string): boolean {
return !this.isSupportedLanguage(uri, language) || this.isUserExcluded(uri)
}

private isSupportedLanguage(uri: string, language?: string): boolean {
language ??= this.guessLanguageID(uri)

const languageOptions = this.meta.getLanguageOptions(language)
if (!languageOptions) {
// Unsupported language.
return false
}

const { uriPredicate } = languageOptions
return uriPredicate?.(uri, UriPredicateContext.create(this)) ?? true
}

/**
* Guess a language ID from a URI. The guessed language ID may or may not actually be supported.
*/
private guessLanguageID(uri: string): string {
const ext = fileUtil.extname(uri) ?? '.spyglassmc-unknown'
return this.meta.getLanguageID(ext) ?? ext.slice(1)
}

private isUserExcluded(uri: string): boolean {
if (this.config.env.exclude.length === 0) {
return false
}
Expand Down
94 changes: 59 additions & 35 deletions packages/java-edition/src/binder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
UriBinder,
UriBinderContext,
UriBuilder,
UriPredicate,
} from '@spyglassmc/core'
import {
ErrorSeverity,
Expand Down Expand Up @@ -210,6 +211,53 @@ export function* getRoots(
return undefined
}

function getCandidateResourcesForRel(rel: string): [res: Resource, identifier: string][] {
const parts = rel.split('/')
if (parts.length < 3) {
return []
}
const [pack, _namespace, ...rest] = parts
if (pack !== 'data' && pack !== 'assets') {
return []
}
const candidateResources: [Resource, string][] = []
if (rest.length === 1) {
const resources = Resources.get('')
for (const res of resources ?? []) {
if (res.pack !== pack) {
continue
}
let identifier = rest[0]
if (!identifier.endsWith(res.ext)) {
continue
}
identifier = identifier.slice(0, -res.ext.length)
if (res.identifier && identifier !== res.identifier) {
continue
}
candidateResources.push([res, identifier])
}
}
for (let i = 1; i < rest.length; i += 1) {
const resources = Resources.get(rest.slice(0, i).join('/'))
for (const res of resources ?? []) {
if (res.pack !== pack) {
continue
}
let identifier = rest.slice(i).join('/')
if (!identifier.endsWith(res.ext)) {
continue
}
identifier = identifier.slice(0, -res.ext.length)
if (res.identifier && identifier !== res.identifier) {
continue
}
candidateResources.push([res, identifier])
}
}
return candidateResources
}

export function dissectUri(uri: string, ctx: UriBinderContext) {
const rels = getRels(uri, ctx.roots)
const release = ctx.project['loadedVersion'] as ReleaseVersion | undefined
Expand All @@ -226,41 +274,7 @@ export function dissectUri(uri: string, ctx: UriBinderContext) {
if (pack !== 'data' && pack !== 'assets') {
continue
}
const candidateResources: [Resource, string][] = []
if (rest.length === 1) {
const resources = Resources.get('')
for (const res of resources ?? []) {
if (res.pack !== pack) {
continue
}
let identifier = rest[0]
if (!identifier.endsWith(res.ext)) {
continue
}
identifier = identifier.slice(0, -res.ext.length)
if (res.identifier && identifier !== res.identifier) {
continue
}
candidateResources.push([res, identifier])
}
}
for (let i = 1; i < rest.length; i += 1) {
const resources = Resources.get(rest.slice(0, i).join('/'))
for (const res of resources ?? []) {
if (res.pack !== pack) {
continue
}
let identifier = rest.slice(i).join('/')
if (!identifier.endsWith(res.ext)) {
continue
}
identifier = identifier.slice(0, -res.ext.length)
if (res.identifier && identifier !== res.identifier) {
continue
}
candidateResources.push([res, identifier])
}
}
const candidateResources = getCandidateResourcesForRel(rel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic between const parts = res.split('/') and here is also duplicated in getCandidateResourcesForRel

Copy link
Member Author

@SPGoding SPGoding Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's cuz we use the value of namespace below and I did not want to make getCandidateResourcesForRel() return two things (getCandidateResourcesAndNamespaceForRel()?).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I see. Though it already returns a list of two things: function getCandidateResourcesForRel(rel: string): [res: Resource, identifier: string][], so maybe it wouldn't be that bad to have it also return the namespace in that tuple (or change to an object)?

if (candidateResources.length === 0) {
continue
}
Expand Down Expand Up @@ -386,3 +400,13 @@ export function registerUriBuilders(meta: MetaRegistry) {
meta.registerUriBuilder(category, uriBuilder(resources))
}
}

/**
* Returns true for JSON file URIs that belong to any known resource category. No version check is
* performed as we would like to provide errors even for files in the wrong folder or files for the
* wrong version.
*/
export const jsonUriPredicate: UriPredicate = (uri, ctx) => {
const rels = [...getRels(uri, ctx.roots)]
return rels.some((rel) => getCandidateResourcesForRel(rel).length > 0)
}
5 changes: 2 additions & 3 deletions packages/java-edition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as core from '@spyglassmc/core'
import * as json from '@spyglassmc/json'
import * as mcdoc from '@spyglassmc/mcdoc'
import * as nbt from '@spyglassmc/nbt'
import { registerUriBuilders, uriBinder } from './binder/index.js'
import { jsonUriPredicate, registerUriBuilders, uriBinder } from './binder/index.js'
import type { McmetaSummary, McmetaVersions, PackInfo } from './dependency/index.js'
import {
getMcmetaSummary,
Expand Down Expand Up @@ -161,7 +161,6 @@ export const initialize: core.ProjectInitializer = async (ctx) => {

registerMcdocAttributes(meta, summary.commands, release)
registerPackFormatAttribute(meta, release, versions, packs)
registerPackFormatAttribute(meta, release, versions, packs)

meta.registerLanguage('zip', { extensions: ['.zip'] })
meta.registerLanguage('png', { extensions: ['.png'] })
Expand All @@ -171,7 +170,7 @@ export const initialize: core.ProjectInitializer = async (ctx) => {
meta.registerLanguage('fsh', { extensions: ['.fsh'] })
meta.registerLanguage('vsh', { extensions: ['.vsh'] })

json.initialize(ctx)
json.getInitializer(jsonUriPredicate)(ctx)
jeJson.initialize(ctx)
jeMcf.initialize(ctx, summary.commands, release)
nbt.initialize(ctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ describe('mcfunction argument parser', () => {
properties,
}
const project = mockProjectData({ ctx: { loadedVersion: version } })
json.initialize(project)
json.getInitializer()(project)
nbt.initialize(project)

for (const string of content) {
Expand Down
Loading